Created
March 9, 2018 17:18
-
-
Save lynn/fe5b2a82cf021a05fd0733e08e321ad2 to your computer and use it in GitHub Desktop.
Patching new music into Super Mario Bros.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
######################################## | |
# Utility functions | |
######################################## | |
def cpu_to_rom(addr: int) -> int: | |
return addr - 0x8000 + 0x10 | |
def rom_to_cpu(addr: int) -> int: | |
return addr + 0x8000 - 0x10 | |
def text_chr(s: str) -> bytes: | |
return bytes(map('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ -× !'.index, s)) | |
# Return the amount of bytes written. | |
def write(b: bytearray, index: int, length: int, data) -> int: | |
assert len(data) == length | |
b[index:index+length] = data | |
return length | |
def write_padding(b: bytearray, index: int, length: int, data) -> int: | |
assert len(data) <= length | |
return write(b, index, length, data + bytes(length - len(data))) | |
######################################## | |
# Color patches | |
######################################## | |
# Background color addresses: | |
water = 0x5Df | |
day_sky = 0x5E0 | |
night_sky = 0x5E1 | |
# Object color addresses: | |
ow_bushes_bright = 0xCDC | |
ow_bushes_dark = 0xCDD | |
ow_bushes_outline = 0xCDE | |
ow_brick_bright = 0xCE0 | |
ow_brick_dark = 0xCE1 | |
mario_hat = 0x5E8 | |
mario_skin = 0x5E9 | |
mario_hair = 0x5EA | |
def patch_colors(rom: bytearray) -> None: | |
rom[water] = 0x14 | |
rom[day_sky] = 0x2B | |
# rom[night_sky] = navy = 0x01 | |
rom[ow_bushes_dark] = 0x15 | |
rom[ow_bushes_bright] = 0x25 | |
# rom[ow_brick_dark] = teal = 0x17 | |
# rom[ow_brick_bright] = bright_teal = 0x27 | |
rom[mario_skin] = 0x27 | |
rom[mario_hair] = 0x01 | |
######################################## | |
# Music patches! | |
# We use strings like "C-5" or "C#5" to represent notes, | |
# strings "4.", "4", "8.", "8", "8t", "16", "16t", "32t" for note duration opcodes, | |
# "0" for stop, "." for rest | |
######################################## | |
song_table = 0x791D | |
song_table_size = 49 | |
overworld_pattern_list = 0x792D | |
song_headers = song_table + song_table_size | |
music_data = 0x79C8 # Adjust as needed. | |
music_data_size = 1352 | |
# Speed values. | |
bpm150 = 0x20 | |
bpm100 = 0x18 | |
melody_to_byte = { | |
'q.': 0x80, 'q': 0x86, 'i.': 0x85, 'i': 0x84, 'it': 0x87, 's': 0x82, 'st': 0x83, 'z': 0x81, # (From ZZT #PLAY! ^v^) | |
'G-6': 0x58, 'E-6': 0x56, 'D-6': 0x02, 'C-6': 0x54, 'Bb5': 0x52, 'A#5': 0x52, 'Ab5': 0x50, 'G#5': 0x50, 'G-5': 0x4E, | |
'F-5': 0x4C, 'E-5': 0x44, 'D#5': 0x4A, 'D-5': 0x48, 'Db5': 0x46, 'C#5': 0x46, 'C-5': 0x64, 'B-4': 0x42, 'Bb4': 0x3E, | |
'A#4': 0x3E, 'A-4': 0x40, 'Ab4': 0x3C, 'G#4': 0x3C, 'G-4': 0x3A, 'Gb4': 0x38, 'F#4': 0x38, 'F-4': 0x36, 'E-4': 0x34, | |
'Eb4': 0x32, 'D#4': 0x32, 'D-4': 0x30, 'Db4': 0x2E, 'C#4': 0x2E, 'C-4': 0x2C, 'B-3': 0x2A, 'Bb3': 0x28, 'A#3': 0x28, | |
'A-3': 0x26, 'Ab3': 0x24, 'G#3': 0x24, 'G-3': 0x22, 'Gb3': 0x20, 'F#3': 0x20, 'F-3': 0x1E, 'E-3': 0x1C, 'Eb3': 0x1A, | |
'D#3': 0x1A, 'D-3': 0x18, 'C#3': 0x16, 'C-3': 0x14, 'B-2': 0x12, 'Bb2': 0x10, 'A#2': 0x10, 'A-2': 0x62, 'Ab2': 0x0E, | |
'G#2': 0x0E, 'G-2': 0x0C, 'Gb2': 0x0A, 'F#2': 0x0A, 'F-2': 0x08, 'E-2': 0x06, 'Eb2': 0x60, 'D#2': 0x60, 'D-2': 0x5E, | |
'C-2': 0x5C, 'G-2': 0x5A, 'x': 0x04, '0': 0x00, | |
} | |
harmony_to_byte = { | |
# Drums (Open hat, Kick, Closed hat): | |
'q.O': 0x30, 'qO': 0xB1, 'i.O': 0x71, 'iO': 0x31, 'itO': 0xF1, 'sO': 0xB0, 'stO': 0xF0, 'zO': 0x70, | |
'q.K': 0x20, 'qK': 0xA1, 'i.K': 0x61, 'iK': 0x21, 'itK': 0xE1, 'sK': 0xA0, 'stK': 0xE0, 'zK': 0x60, | |
'q.C': 0x10, 'qC': 0x91, 'i.C': 0x51, 'iC': 0x11, 'itC': 0xD1, 'sC': 0x90, 'stC': 0xD0, 'zC': 0x50, | |
# Square 1 notes: | |
'q.Bb4': 0x3E, 'qBb4': 0xBF, 'i.Bb4': 0x7F, 'iBb4': 0x3F, 'itBb4': 0xFF, 'sBb4': 0xBE, 'stBb4': 0xFE, 'zBb4': 0x7E, | |
'q.A#4': 0x3E, 'qA#4': 0xBF, 'i.A#4': 0x7F, 'iA#4': 0x3F, 'itA#4': 0xFF, 'sA#4': 0xBE, 'stA#4': 0xFE, 'zA#4': 0x7E, | |
'q.Ab4': 0x3C, 'qAb4': 0xBD, 'i.Ab4': 0x7D, 'iAb4': 0x3D, 'itAb4': 0xFD, 'sAb4': 0xBC, 'stAb4': 0xFC, 'zAb4': 0x7C, | |
'q.G#4': 0x3C, 'qG#4': 0xBD, 'i.G#4': 0x7D, 'iG#4': 0x3D, 'itG#4': 0xFD, 'sG#4': 0xBC, 'stG#4': 0xFC, 'zG#4': 0x7C, | |
'q.G-4': 0x3A, 'qG-4': 0xBB, 'i.G-4': 0x7B, 'iG-4': 0x3B, 'itG-4': 0xFB, 'sG-4': 0xBA, 'stG-4': 0xFA, 'zG-4': 0x7A, | |
'q.Gb4': 0x38, 'qGb4': 0xB9, 'i.Gb4': 0x79, 'iGb4': 0x39, 'itGb4': 0xF9, 'sGb4': 0xB8, 'stGb4': 0xF8, 'zGb4': 0x78, | |
'q.F#4': 0x38, 'qF#4': 0xB9, 'i.F#4': 0x79, 'iF#4': 0x39, 'itF#4': 0xF9, 'sF#4': 0xB8, 'stF#4': 0xF8, 'zF#4': 0x78, | |
'q.F-4': 0x36, 'qF-4': 0xB7, 'i.F-4': 0x77, 'iF-4': 0x37, 'itF-4': 0xF7, 'sF-4': 0xB6, 'stF-4': 0xF6, 'zF-4': 0x76, | |
'q.E-4': 0x34, 'qE-4': 0xB5, 'i.E-4': 0x75, 'iE-4': 0x35, 'itE-4': 0xF5, 'sE-4': 0xB4, 'stE-4': 0xF4, 'zE-4': 0x74, | |
'q.Eb4': 0x32, 'qEb4': 0xB3, 'i.Eb4': 0x73, 'iEb4': 0x33, 'itEb4': 0xF3, 'sEb4': 0xB2, 'stEb4': 0xF2, 'zEb4': 0x72, | |
'q.D#4': 0x32, 'qD#4': 0xB3, 'i.D#4': 0x73, 'iD#4': 0x33, 'itD#4': 0xF3, 'sD#4': 0xB2, 'stD#4': 0xF2, 'zD#4': 0x72, | |
'q.D-4': 0x30, 'qD-4': 0xB1, 'i.D-4': 0x71, 'iD-4': 0x31, 'itD-4': 0xF1, 'sD-4': 0xB0, 'stD-4': 0xF0, 'zD-4': 0x70, | |
'q.C#4': 0x2E, 'qC#4': 0xAF, 'i.C#4': 0x6F, 'iC#4': 0x2F, 'itC#4': 0xEF, 'sC#4': 0xAE, 'stC#4': 0xEE, 'zC#4': 0x6E, | |
'q.C-4': 0x2C, 'qC-4': 0xAD, 'i.C-4': 0x6D, 'iC-4': 0x2D, 'itC-4': 0xED, 'sC-4': 0xAC, 'stC-4': 0xEC, 'zC-4': 0x6C, | |
'q.B-3': 0x2A, 'qB-3': 0xAB, 'i.B-3': 0x6B, 'iB-3': 0x2B, 'itB-3': 0xEB, 'sB-3': 0xAA, 'stB-3': 0xEA, 'zB-3': 0x6A, | |
'q.Bb3': 0x28, 'qBb3': 0xA9, 'i.Bb3': 0x69, 'iBb3': 0x29, 'itBb3': 0xE9, 'sBb3': 0xA8, 'stBb3': 0xE8, 'zBb3': 0x68, | |
'q.A#3': 0x28, 'qA#3': 0xA9, 'i.A#3': 0x69, 'iA#3': 0x29, 'itA#3': 0xE9, 'sA#3': 0xA8, 'stA#3': 0xE8, 'zA#3': 0x68, | |
'q.A-3': 0x26, 'qA-3': 0xA7, 'i.A-3': 0x67, 'iA-3': 0x27, 'itA-3': 0xE7, 'sA-3': 0xA6, 'stA-3': 0xE6, 'zA-3': 0x66, | |
'q.Ab3': 0x24, 'qAb3': 0xA5, 'i.Ab3': 0x65, 'iAb3': 0x25, 'itAb3': 0xE5, 'sAb3': 0xA4, 'stAb3': 0xE4, 'zAb3': 0x64, | |
'q.G#3': 0x24, 'qG#3': 0xA5, 'i.G#3': 0x65, 'iG#3': 0x25, 'itG#3': 0xE5, 'sG#3': 0xA4, 'stG#3': 0xE4, 'zG#3': 0x64, | |
'q.G-3': 0x22, 'qG-3': 0xA3, 'i.G-3': 0x63, 'iG-3': 0x23, 'itG-3': 0xE3, 'sG-3': 0xA2, 'stG-3': 0xE2, 'zG-3': 0x62, | |
'q.Gb3': 0x20, 'qGb3': 0xA1, 'i.Gb3': 0x61, 'iGb3': 0x21, 'itGb3': 0xE1, 'sGb3': 0xA0, 'stGb3': 0xE0, 'zGb3': 0x60, | |
'q.F#3': 0x20, 'qF#3': 0xA1, 'i.F#3': 0x61, 'iF#3': 0x21, 'itF#3': 0xE1, 'sF#3': 0xA0, 'stF#3': 0xE0, 'zF#3': 0x60, | |
'q.F-3': 0x1E, 'qF-3': 0x9F, 'i.F-3': 0x5F, 'iF-3': 0x1F, 'itF-3': 0xDF, 'sF-3': 0x9E, 'stF-3': 0xDE, 'zF-3': 0x5E, | |
'q.E-3': 0x1C, 'qE-3': 0x9D, 'i.E-3': 0x5D, 'iE-3': 0x1D, 'itE-3': 0xDD, 'sE-3': 0x9C, 'stE-3': 0xDC, 'zE-3': 0x5C, | |
'q.Eb3': 0x1A, 'qEb3': 0x9B, 'i.Eb3': 0x5B, 'iEb3': 0x1B, 'itEb3': 0xDB, 'sEb3': 0x9A, 'stEb3': 0xDA, 'zEb3': 0x5A, | |
'q.D#3': 0x1A, 'qD#3': 0x9B, 'i.D#3': 0x5B, 'iD#3': 0x1B, 'itD#3': 0xDB, 'sD#3': 0x9A, 'stD#3': 0xDA, 'zD#3': 0x5A, | |
'q.D-3': 0x18, 'qD-3': 0x99, 'i.D-3': 0x59, 'iD-3': 0x19, 'itD-3': 0xD9, 'sD-3': 0x98, 'stD-3': 0xD8, 'zD-3': 0x58, | |
'q.Db3': 0x16, 'qDb3': 0x97, 'i.Db3': 0x57, 'iDb3': 0x17, 'itDb3': 0xD7, 'sDb3': 0x96, 'stDb3': 0xD6, 'zDb3': 0x56, | |
'q.C#3': 0x16, 'qC#3': 0x97, 'i.C#3': 0x57, 'iC#3': 0x17, 'itC#3': 0xD7, 'sC#3': 0x96, 'stC#3': 0xD6, 'zC#3': 0x56, | |
'q.C-3': 0x14, 'qC-3': 0x95, 'i.C-3': 0x55, 'iC-3': 0x15, 'itC-3': 0xD5, 'sC-3': 0x94, 'stC-3': 0xD4, 'zC-3': 0x54, | |
'q.B-2': 0x12, 'qB-2': 0x93, 'i.B-2': 0x53, 'iB-2': 0x13, 'itB-2': 0xD3, 'sB-2': 0x92, 'stB-2': 0xD2, 'zB-2': 0x52, | |
'q.Bb2': 0x10, 'qBb2': 0x91, 'i.Bb2': 0x51, 'iBb2': 0x11, 'itBb2': 0xD1, 'sBb2': 0x90, 'stBb2': 0xD0, 'zBb2': 0x50, | |
'q.A#2': 0x10, 'qA#2': 0x91, 'i.A#2': 0x51, 'iA#2': 0x11, 'itA#2': 0xD1, 'sA#2': 0x90, 'stA#2': 0xD0, 'zA#2': 0x50, | |
'q.Ab2': 0x0E, 'qAb2': 0x8F, 'i.Ab2': 0x4F, 'iAb2': 0x0F, 'itAb2': 0xCF, 'sAb2': 0x8E, 'stAb2': 0xCE, 'zAb2': 0x4E, | |
'q.G#2': 0x0E, 'qG#2': 0x8F, 'i.G#2': 0x4F, 'iG#2': 0x0F, 'itG#2': 0xCF, 'sG#2': 0x8E, 'stG#2': 0xCE, 'zG#2': 0x4E, | |
'q.G-2': 0x0C, 'qG-2': 0x8D, 'i.G-2': 0x4D, 'iG-2': 0x0D, 'itG-2': 0xCD, 'sG-2': 0x8C, 'stG-2': 0xCC, 'zG-2': 0x4C, | |
'q.Gb2': 0x0A, 'qGb2': 0x8B, 'i.Gb2': 0x4B, 'iGb2': 0x0B, 'itGb2': 0xCB, 'sGb2': 0x8A, 'stGb2': 0xCA, 'zGb2': 0x4A, | |
'q.F#2': 0x0A, 'qF#2': 0x8B, 'i.F#2': 0x4B, 'iF#2': 0x0B, 'itF#2': 0xCB, 'sF#2': 0x8A, 'stF#2': 0xCA, 'zF#2': 0x4A, | |
'q.F-2': 0x08, 'qF-2': 0x89, 'i.F-2': 0x49, 'iF-2': 0x09, 'itF-2': 0xC9, 'sF-2': 0x88, 'stF-2': 0xC8, 'zF-2': 0x48, | |
'q.E-2': 0x06, 'qE-2': 0x87, 'i.E-2': 0x47, 'iE-2': 0x07, 'itE-2': 0xC7, 'sE-2': 0x86, 'stE-2': 0xC6, 'zE-2': 0x46, | |
'q.x': 0x04, 'qx': 0x85, 'i.x': 0x45, 'ix': 0x05, 'itx': 0xC5, 'sx': 0x84, 'stx': 0xC4, 'zx': 0x44, | |
'q.D-6': 0x02, 'qD-6': 0x83, 'i.D-6': 0x43, 'iD-6': 0x03, 'itD-6': 0xC3, 'sD-6': 0x82, 'stD-6': 0xC2, 'zD-6': 0x42, | |
'0': 0x00, | |
} | |
byte_to_melody = {v: k for k,v in melody_to_byte.items()} | |
byte_to_harmony = {v: k for k,v in harmony_to_byte.items()} | |
def melody(phrase: str) -> bytes: | |
return bytes(melody_to_byte[s] for s in phrase.split()) | |
def harmony(phrase: str) -> bytes: | |
return bytes(harmony_to_byte[s] for s in phrase.split()) | |
bass = melody | |
noise = harmony | |
class Music: | |
def __init__(self, rom): | |
self.rom = rom | |
self.hp = song_headers # Header pointer | |
self.dp = music_data # Data pointer | |
def write(self, index, length, data) -> int: | |
return write(self.rom, index, length, data) | |
def write_data(self, buf) -> int: | |
'''Write data, advance dp, return old dp.''' | |
where = self.dp | |
self.dp += self.write(where, len(buf), buf) | |
return where | |
def write_header(self, buf) -> int: | |
'''Write data, advance hp, return old hp.''' | |
where = self.hp | |
print(hex(where)) | |
self.hp += self.write(where, len(buf), buf) | |
return where | |
def song(self, name: str, speed: int, melody: bytes, harmony: bytes, bass: bytes, noise: bytes) -> int: | |
'''Write a song header + data. Return (header - song table), to write to the song table.''' | |
print('=== Writing song: %r' % name) | |
# Write song data. | |
noise = noise or b'\4' # Avoid crash. | |
parts = {} | |
m = self.write_data(melody + b'\0'); parts[melody] = m | |
b = parts.get(bass) or self.write_data(bass); parts[bass] = b | |
h = parts.get(harmony) or self.write_data(harmony); parts[harmony] = h | |
n = parts.get(noise) or self.write_data(noise + b'\0'); parts[noise] = n | |
print('(Mel, Bass, Harm, Noise) at (%04x, %04x, %04x, %04x)' % (m, b, h, n)) | |
# Compute the header. | |
cpu_m = rom_to_cpu(m) | |
lo = cpu_m & 0xFF | |
hi = cpu_m >> 8 | |
tr = b - m | |
s1 = h - m | |
ns = n - m | |
# Write header. | |
header = bytes([speed, lo, hi, tr, s1] + ([] if ns is None else [ns])) | |
addr = self.write_header(header) | |
print('Header:', ' '.join('%02x' % c for c in header), 'at', addr) | |
print('Use as %02x in the song table.' % (addr - song_table)) | |
return addr - song_table | |
def clear(self) -> None: | |
silence = self.song('Silence', 0x18, b'\4', b'\4', b'\4', b'\4') | |
# Point everything towards silence. | |
self.write(song_table, song_table_size, bytes([silence] * song_table_size)) | |
# Overworld music! | |
ow = overworld_pattern_list | |
self.rom[ow] = \ | |
self.song('Overworld Intro', bpm150, | |
melody('q E-4 F-4 G-4'), | |
harmony('qC-4 qD-4 qE-4'), | |
bass('q G-3 A-3 C-4'), | |
noise('qK qK qO')) | |
self.rom[ow+1] = self.rom[ow+2] = self.rom[ow+3] = self.rom[ow+4] = \ | |
self.song('Overworld Main', bpm150, | |
melody('q. Bb4 i G-4 x Bb4 q A-4 G-4 F-4 q. G-4 i E-4 x F-4 q E-4 D-4 C-4'), | |
harmony('q.G-4 q.E-4 qF-4 qE-4 qD-4 q.C-4 q.G-3 q.Bb3 q.G-3'), | |
bass('q. Bb3 F-4 A-3 E-4 G-3 C-4 i Bb3 C-4 Bb3 A-3 Bb3 C-4'), | |
noise('qK iC qO iC iK iK iO iK qC')) | |
print(self.rom[ow]) | |
def patch(self): | |
self.clear() | |
# def patch_overworld_music(rom: bytearray) -> None: | |
# intro_pattern = rom[overworld_pattern_list + 0] | |
# intro_header = song_table + intro_pattern | |
# sp, lo, hi, tr, s1, ns = rom[intro_header : intro_header+6] | |
# # print([hex(x) for x in [sp,lo,hi,tr,s1,ns]]) | |
# | |
# ima = intro_melody_address = cpu_to_rom(256*hi + lo) | |
# iba = intro_bass_address = ima + tr | |
# iha = intro_harmony_address = ima + s1 | |
# ina = intro_noise_address = ima + ns | |
# print(ima, iba, iha, ina) | |
# | |
# i = ima = intro_melody_address | |
# while rom[i] != 0: print(byte_to_melody[rom[i]], end=' '); i += 1 | |
# print() | |
# | |
# i = iba | |
# while i != ina: print(byte_to_melody[rom[i]], end=' '); i += 1 | |
# print() | |
# | |
# i = iha | |
# while i != iba: print(byte_to_harmony[rom[i]], end=' '); i += 1 | |
# print() | |
# | |
# write(rom, ima, 12, melody('s F-4 i E-4 D-4 s C-4 i A-3 q A-4 G-4')) | |
# write(rom, iha, 1, harmony('sF-3')) | |
# write(rom, ina, 3, harmony('qK qC qO')) | |
def patch_music(rom: bytearray) -> None: | |
Music(rom).patch() | |
######################################## | |
# Main | |
######################################## | |
def patch(rom: bytearray) -> None: | |
patch_colors(rom) | |
patch_music(rom) | |
# Credits (14 bytes): | |
# rom[0x09FB5 : 0x09FC3] = text_chr('LYNN VERSION'.center(14)) | |
def main(output_path: str, input_path: str='Super Mario Bros. (JU) (PRG0) [!].nes') -> None: | |
with open(input_path, 'rb') as f: | |
rom = bytearray(f.read()) | |
assert bytes(rom[0:3]) == b'NES' | |
patch(rom) | |
with open(output_path, 'wb') as f: | |
f.write(rom) | |
if __name__ == '__main__': | |
main('lynnsmb.nes') | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment