def sfx2info(romfile, musyx_position, sfxnames): #Compatible with MusyX > GM2SONG 1.03 and possibly other versions # #Arguments: # romfile = rom file path # musyx_position = position in the file of MusyX. # MusyX is placed at the beginning of a rom bank chosen by the original developer # e.g. in Magi Nation, MusyX is placed at rom bank 0x30 # sfxnames = An empty array, or an array of sfx names that will be used as savefiles with open(romfile, 'rb') as f: rom = f.read() projectdata_pos = musyx_position + snd_ProjectData - 0x4000 sfxtable_address = projectdata_pos + littledata_to_word( rom, projectdata_pos + sdp_SFXTableAddress) sfxtable_len = rom[projectdata_pos + sdp_NumberOfSFXs] targetdir_base = "python/out/" os.makedirs(targetdir_base, exist_ok=True) with open(targetdir_base + "sfxinfo.txt", "w") as f: f.write("{:32} - {:3} {:3} {:3} {:3}\n".format("Name", "Macro", "Pri", "Key", "Vel")) for i in range(sfxtable_len): if (i < len(sfxnames)): name = "sfx_{:03}_{}".format(transform_id(i, ID_SFX), sfxnames[i]) else: name = "sfx_{:03}".format(transform_id(i, ID_SFX)) print(name) pos = sfxtable_address + i * 4 index = transform_id(rom[pos], ID_MACRO) priority = rom[pos + 1] defkey = rom[pos + 2] defvel = rom[pos + 3] * 8 f.write("{:32} - {:3} {:3} {:3} {:3}\n".format( name, index, priority, defkey, defvel))
def adsr2mxt(romfile, musyx_position, adsrnames): #Designed for MUConv.exe v1.04 #Takes rom data and converts back to the equivalent mxt files #The generated mxt files should compile back into exactly the same data #That being said, some parameters could be slightly different from the original mxt file as multiple mxt values sometimes round to the same data when parsed #Arguments: # romfile = rom file path # musyx_position = position in the file of MusyX. # MusyX is placed at the beginning of a rom bank chosen by the original developer # e.g. in Magi Nation, MusyX is placed at rom bank 0x30 # adsrnames = An empty array, or an array of adsr names that will be used as savefiles with open(romfile, 'rb') as f: rom = f.read() projectdata_pos = musyx_position + snd_ProjectData - 0x4000 adsrtable_address_pos = projectdata_pos + sdp_ADSRTableAddress sfx_address_pos = projectdata_pos + sdp_SFXTableAddress adsrbase_offset = littledata_to_word(rom, adsrtable_address_pos) sfxbase_offset = littledata_to_word(rom, sfx_address_pos) if (adsrbase_offset == sfxbase_offset): print("There seems to be 0 ADSR entries. Aborting") return adsrbase_pos = adsrbase_offset + projectdata_pos targetdir = "python/out/Tables/" os.makedirs(targetdir, exist_ok=True) i = 0 adsrs = [] while True: if (i < len(adsrnames)): name = "adsr_{:03}_{}".format(transform_id(i, ID_ADSR), adsrnames[i]) else: name = "adsr_{:03}".format(transform_id(i, ID_ADSR)) print(name) newaddress = littledata_to_word(rom, adsrbase_pos + i * 2) data_pos = newaddress + adsrbase_pos if (i == 0): adsrtable_endpos = newaddress + adsrbase_pos with open(targetdir + name + ".mxt", "wb") as f: data = [ littledata_to_word(rom, data_pos + 0), littledata_to_word(rom, data_pos + 2), rom[data_pos + 4], littledata_to_word(rom, data_pos + 5) ] for d in [1, 3]: #Signed values if (data[d] >= 256**2 // 2): data[d] = data[d] - 256**2 attack = _solve_cycles(math.floor(3840 / (data[0] - 0.5))) decay = _solve_cycles((-1) * (0x0F - data[2]) * 0x100 // data[1]) sustain = math.ceil((data[2] - 0.5) / 0.003662109375) release = _solve_cycles((-1) * data[2] * 0x100 // data[3]) f.write(twobytearray(attack)) f.write(twobytearray(decay)) f.write(twobytearray(sustain)) f.write(twobytearray(release)) i += 1 if (adsrbase_pos + i * 2 == adsrtable_endpos) or ( i > 256): #Quit when you reach the address of the first adsr break
def song2wav(songfile_name, musyx_position, songnames, debug=False): #Compatible with MusyX > GM2SONG 1.03 and possibly other versions # #Arguments: # songfile_name = rom file path # musyx_position = position in the file of MusyX. # MusyX is placed at the beginning of a rom bank chosen by the original developer # e.g. in Magi Nation, MusyX is placed at rom bank 0x30 # songnames = An empty array, or an array of song names that will be used as savefiles with open(songfile_name, 'rb') as f: songfile = f.read() projectdata_pos = musyx_position + snd_ProjectData - 0x4000 songtable_pos = projectdata_pos + littledata_to_word( songfile, projectdata_pos + sdp_SongTableAddress ) #data_to_word(songfile[projectdata_pos+sdp_SongTableAddress:projectdata_pos+sdp_SongTableAddress+2]) number_of_songs = songfile[projectdata_pos + sdp_NumberOfSongs] print("{} songs".format(number_of_songs)) last_songhash = None songlist_i = -1 os.makedirs(targetdir, exist_ok=True) for i in range(number_of_songs - 1, -1, -1): if (i < len(songnames)): name = "song_{:03}_{}".format(transform_id(i, ID_SONG), songnames[i]) else: name = "song_{:03}".format(transform_id(i, ID_SONG)) print(name) songlookup_pos = songtable_pos + 3 * i relativebank = songfile[songlookup_pos] address = +littledata_to_word( songfile, songlookup_pos + 1) #data_to_word(songfile[songlookup_pos+1:songlookup_pos+3]) song_pos = musyx_position + relativebank * 0x4000 + address - 0x4000 + 0x84 filesize, sha = song2gm(songfile_name, song_pos, name + ".mid") if (debug): with open(targetdir + name + "_ori.dat", "wb") as f: f.write(songfile[song_pos:song_pos + filesize]) else: os.remove(targetdir + name + "_new.dat") if (hashlib.sha256(songfile[song_pos:song_pos + filesize]).digest() != sha): print("NOT IDENTICAL") song_pos = musyx_position + relativebank * 0x4000 + address - 0x4000 #header info defaults = [ transform_id(songfile[song_pos + j], ID_MACRO) for j in range(4) ] song_pos += 4 soundlist = [ transform_id(songfile[song_pos + j], ID_MACRO) for j in range(0x80) ] soundhash = hashlib.sha256(bytes(soundlist)).hexdigest() if soundhash != last_songhash: songlist_i += 1 last_songhash = soundhash nullprg = transform_id(0, ID_MACRO) soundlist_2 = soundlist.copy() for j in range(len(soundlist_2)): if (soundlist_2[j] == nullprg): soundlist_2[j] = "{:3} or undefined".format(nullprg) with open( targetdir_base + "soundlist_{:02}.txt".format(songlist_i), "w") as f: for j in range(len(soundlist_2)): f.write("Soundlist {} .mxm id: {:3}\n".format( j + 1, soundlist_2[j])) f.write("\n\n") with open(targetdir_base + "soundlist_{:02}.txt".format(songlist_i), "a") as f: f.write(name + "\n") for j in range(len(defaults)): success = False for k in range(len(soundlist)): if (defaults[j] == soundlist[k]): f.write("MidiSetup Channel {} Pgr.: {:3}\n".format( j + 1, k + 1)) success = True break if (not (success)): f.write("MidiSetup Channel {} Pgr.: UNKNOWN?\n".format(j + 1)) f.write("\n")
def macro2mxm(romfile, musyx_position, macronames): #Designed for MUConv.exe v1.04 #Takes rom data and converts back to the equivalent mxm files #The generated mxm files should compile back into exactly the same data #That being said, some parameters could be slightly different from the original mxm file as multiple mxm values sometimes round to the same data when parsed #Arguments: # songfile_name = rom file path # musyx_position = position in the file of MusyX. # MusyX is placed at the beginning of a rom bank chosen by the original developer # e.g. in Magi Nation, MusyX is placed at rom bank 0x30 # macronames = An empty array, or an array of macro names that will be used as savefiles with open(romfile, 'rb') as f: rom = f.read() projectdata_pos = musyx_position + snd_ProjectData - 0x4000 macrobase_pos = projectdata_pos + sdp_SoundMacroLookupTable macrosamplemap_pos = projectdata_pos + sdp_SampleMapMacro_Address macrosamplemap_n_pos = projectdata_pos + sdp_NumberOfSampleMapEntries samplemap_pos = littledata_to_word( rom, macrosamplemap_pos) - sdp_SoundMacroLookupTable samplemap_n = rom[macrosamplemap_n_pos] samplemap_found = False i = 0 macros = [] while True: samplemap_entries = 0 newaddress = littledata_to_word(rom, macrobase_pos + i * 2) if (i == 0): macrotable_endpos = newaddress + macrobase_pos if (newaddress == samplemap_pos): samplemap_entries = samplemap_n samplemap_found = True macros.append( SoundMacro(musyx_position, newaddress, samplemap_entries, i)) i += 1 if (macrobase_pos + i * 2 == macrotable_endpos) or ( i > 256): #Quit when you reach the address of the first macro break for i in range(len(macros)): if (i < len(macronames)): name = "macro_{:03}_{}".format(transform_id(i, ID_MACRO), macronames[i]) else: name = "macro_{:03}".format(transform_id(i, ID_MACRO)) macros[i].name = name macros[i].init_steps(rom) for i in range(len(macros)): macros[i].mxm_steps(macros) targetdir_base = "python/out/" targetdir = "python/out/Soundmacros/" os.makedirs(targetdir, exist_ok=True) for macro in macros: print(macro.name) with open(targetdir + macro.name + ".mxm", "wb") as f: for step in macro.steps: f.write(step.mxm) with open(targetdir_base + "macrodebug.txt", "w") as f: for macro in macros: f.write("{:16} {:06X}\n".format( macro.name, macro.address + sdp_SoundMacroLookupTable)) for step in macro.steps: f.write(step.debug()) f.write("\n\n")
def create_mxm(self, macros): if (self.samplemap): self.store(0x24, 0) id = self.val(0) id = transform_id(id, ID_SAMPLE) self.store(id, 1, 2) else: opcode = self.val(0) command_lengths.pop(opcode, None) if (opcode == 0x00): #END self.store(0x00, 0) elif (opcode == 0x23): #STOP self.store(0x01, 0) elif (opcode == 0x15): #SPLITKEY self.store(0x02, 0) key = self.val(1) self.store(key, 1) id, step = self.solve_address(macros, 2) id = transform_id(id, ID_MACRO) self.store(id, 2, 2) self.store(step, 4, 2) elif (opcode == 0x17): #SPLITVEL self.store(0x03, 0) vel = self.val(1) self.store(vel, 1) id, step = self.solve_address(macros, 2) id = transform_id(id, ID_MACRO) self.store(id, 2, 2) self.store(step, 4, 2) elif (opcode == 0x20): #RESET_MOD self.store(0x04, 0) elif (opcode == 0x05): #LOOP self.store(0x05, 0) times = self.val(1) self.store(times, 6) step = self.solve_relative(macros, self.parent_index, self.step, 2) self.store(step, 4, 2) elif (opcode == 0x06): #GOTO self.store(0x06, 0) id, step = self.solve_address(macros, 1) id = transform_id(id, ID_MACRO) self.store(id, 2, 2) self.store(step, 4, 2) elif (opcode == 0x04): #WAIT self.store(0x07, 0) bool1 = self.val(1) self.store(bool1, 1) bool2 = self.val(2) self.store(bool2, 2) cycles = self.val(3, 2) if (cycles == 0x0001 and bool2 == 1): milli = 0xFFFF elif (cycles == 0x0000): milli = 0xFFFF else: milli = self.solve_cycles(3) - 1 self.store(milli, 6, 2) elif (opcode == 0x26): #PLAYMACRO self.store(0x08, 0) voice = self.val(1) if (voice != 0xFF): voice = voice & 0b11 self.store(voice, 1) id = self.val(2) id = transform_id(id, ID_MACRO) self.store(id, 2, 2) bool = self.bit(1, 7) self.store(bool, 4) elif (opcode == 0x22): #PLAYKEYSAMPLE self.store(0x09, 0) elif (opcode == 0x1F): #STOP_MOD self.store(0x0A, 0) elif (opcode == 0x0E): #SETVOICE self.store(0x0B, 0) voice = self.val(1) if (voice == 0xFF): self.store(0xFF, 1) self.store(0, 4) self.store(0, 5) else: voice = voice & 0b11 macros[ self. parent_index].predictedvoice = voice # Set the predicted voice to discriminate between VOICE_ON and WAVE_ON self.store(voice, 1) bool1 = self.bit(1, 7) self.store(bool1, 4) bool2 = self.bit(1, 4) self.store(bool2, 5) elif (opcode == 0x0F): #SETADSR self.store(0x0C, 0) id = self.val(1) id = transform_id(id, ID_ADSR) self.store(id, 1, 2) elif (opcode == 0x09): #SETVOLUME self.store(0x0D, 0) vol = self.solve_volume(1) self.store(vol, 1) elif (opcode == 0x0A): #PANNING self.store(0x0E, 0) pan = self.val(1) if (pan == 0): self.store(0, 1) elif (pan == 1): self.store(64, 1) elif (pan == 2): self.store(127, 1) else: raise ValueError elif (opcode == 0x11): #ENVELOPE (descending) self.store(0x0F, 0) self.store(0, 1) x = int(0x0F00 / self.val(1, 2, True)) * (-1) milli = self._solve_cycles(x) self.store(milli, 6, 2) elif (opcode == 0x27): #ENVELOPE (ascending) self.store(0x0F, 0) self.store(1, 1) x = int(0x0F00 / self.val(1, 2, True)) * (1) milli = self._solve_cycles(x) self.store(milli, 6, 2) elif (opcode == 0x21): #STARTSAMPLE self.store(0x10, 0) id = self.val(1) id = transform_id(id, ID_SAMPLE) self.store(id, 1, 2) elif (opcode == 0x0D): #VOICE_OFF self.store(0x11, 0) elif (opcode == 0x12): #KEYOFF self.store(0x12, 0) voice = self.val(1) self.store(voice, 1) elif (opcode == 0x16): #SPLITRND self.store(0x13, 0) rand = self.val(1) self.store(rand, 1) id, step = self.solve_address(macros, 2) id = transform_id(id, ID_MACRO) self.store(id, 2, 2) self.store(step, 4, 2) elif (opcode == 0x0C): if (macros[self.parent_index].predictedvoice == 2): #WAVE_ON self.store(0x26, 0) id = self.val(1) if (id == 0xFF): self.store(0, 1) else: id = transform_id(id, ID_SAMPLE) self.store(id, 1, 2) elif (macros[self.parent_index].predictedvoice in [0, 1, 3]): #VOICE_ON self.store(0x14, 0) duty = self.val(1) self.store(duty, 1) else: print( "Macro {:03} - guessing step {} to be VOICE_ON, but it could also be WAVE_ON" .format(self.parent_index, self.step)) self.store(0x14, 0) duty = self.val(1) self.store(duty, 1) elif (opcode == 0x10): #SETNOISE self.store(0x15, 0) macros[ self. parent_index].predictedvoice = 3 # Set the predicted voice byte = self.val(1) clock = (byte & 0b11110000) >> 4 self.store(clock, 1) step = (byte & 0b00001000) >> 3 self.store(step, 2) freq = byte & 0b00000111 self.store(freq, 3) elif (opcode == 0x1B): #PORTLAST self.store(0x16, 0) key, cent = self.solve_keycents(1) self.store(key, 1) self.store(cent, 2) milli = solve_cycles(3) self.store(milli, 6) elif (opcode == 0x0B): #RNDNOTE self.store(0x17, 0) bool1 = self.bit(1, 7) self.store(bool1, 4) bool2 = self.bit(1, 0) self.store(bool2, 5) note1 = self.val(2) self.store(note1, 1) note2 = self.val(3) self.store(note2, 3) cent = self.solve_cents(4) #cent is always positive self.store(cent, 2) elif (opcode == 0x08): #ADDNOTE self.store(0x18, 0) bool = 1 - self.val(1) self.store(bool, 3) add = self.val(2, 1, True) self.store(add, 1) key, cent = self.solve_keycents( 2 ) #technically key is undefined, but maybe cent derives its sign from add (i.e. maybe MusyX has an interpretation bug here - I'm not sure so I'll guess - maybe this is changed in more recent versions of MuConv) self.store(cent, 2) elif (opcode == 0x07): #SETNOTE self.store(0x19, 0) key = self.val(1) self.store(key, 1) key, cent = self.solve_keycents( 1 ) #technically key is undefined, but maybe cent derives its sign from key (i.e. maybe MusyX has an interpretation bug here - I'm not sure so I'll guess - maybe this is changed in more recent versions of MuConv) self.store(cent, 2) elif (opcode == 0x02): #PORTAMENTO self.store(0x1B, 0) key, cent = self.solve_keycents(1) self.store(key, 1) self.store(cent, 2) milli = solve_cycles(3) self.store(milli, 6, 2) bool = self.val(5) self.store(bool, 3) elif (opcode == 0x01): #VIBRATO self.store(0x1C, 0) x = self.val(3) milli = self._solve_cycles(x) milli *= 2 self.store(milli, 6, 2) y_ddiv_x = self.val(1, 2, True) y = y_ddiv_x * x key, cent = self._solve_keycents(y) self.store(key, 1) self.store(cent, 2) elif (opcode == 0x03): #PITCHSWEEP self.store(0x1D, 0) bool = self.bit(5, 7) self.store(bool, 5) key, cent = self.solve_keycents(3) self.store(key, 1) self.store(cent, 2) y = self.val(3, 2, True) y_ddiv_x = self.val(1, 2, True) x = int(y / y_ddiv_x) milli = self._solve_cycles(x) self.store(milli, 6, 2) elif (opcode == 0x13): #HARDENVELOPE self.store(0x1E, 0) bool = self.bit(1, 3) self.store(bool, 1) x = self.val(1) & 0b111 x = math.ceil((x - 0.5) / 0.0042666667141020298004150390625) self.store(x, 6, 2) elif (opcode == 0x1D): #PWN_START macros[ self. parent_index].predictedvoice = 2 # Set the predicted voice self.store(0x1F, 0) low = self.val(1) self.store(low, 1) high = self.val(2) self.store(high, 2) delta = (high - low) * 256 x = self.val(3, 2) if (x == 0): self.store(0, 3, 2) else: cycle = int(delta / x) milli = self._solve_cycles(cycle) self.store(milli, 3, 2) elif (opcode == 0x1E): #PWN_UPDATE macros[ self. parent_index].predictedvoice = 2 # Set the predicted voice self.store(0x20, 0) #same as PWN_START except for this line low = self.val(1) self.store(low, 1) high = self.val(2) self.store(high, 2) delta = (high - low) * 256 x = self.val(3, 2) if (x == 0): self.store(0, 3, 2) else: cycle = int(delta / x) milli = self._solve_cycles(cycle) self.store(milli, 3, 2) elif (opcode == 0x24): #PWN_FIXED macros[ self. parent_index].predictedvoice = 2 # Set the predicted voice self.store(0x21, 0) duty = self.val(1) self.store(duty, 1) elif (opcode == 0x25): #PWN_VELOCITY macros[ self. parent_index].predictedvoice = 2 # Set the predicted voice self.store(0x22, 0) elif (opcode == 0x1A): #SENDFLAG self.store(0x23, 0) flag = self.val(1) self.store(flag, 1) #SAMPLEMAP - see above elif (opcode == 0x28): #CURRENTVOL self.store(0x25, 0) vol = self.solve_volume(1) self.store(vol, 1) #WAVE_ON - see above elif (opcode == 0x29): #ADD_SET_PRIO self.store(0x27, 0) prio = self.val(2) self.store(prio, 1) bool = self.val(1) self.store(bool, 2) elif (opcode == 0x18): #TRAP_KEYOFF self.store(0x28, 0) id, step = self.solve_address(macros, 1) id = transform_id(id, ID_MACRO) self.store(id, 2, 2) self.store(step, 4, 2) elif (opcode == 0x19): #UNTRAP_KEYOFF self.store(0x19, 0) else: raise ValueError
def sample2wav(romfile, musyx_position, samplenames): #Designed for MUConv.exe v1.04 #Takes rom data and converts back to the equivalent .wav files #The generated .wav files should compile back into exactly the same data #That being said, some parameters could be slightly different from the original .wav file as multiple .wav values sometimes round to the same data when parsed #Arguments: # romfile = rom file path # musyx_position = position in the file of MusyX. # MusyX is placed at the beginning of a rom bank chosen by the original developer # e.g. in Magi Nation, MusyX is placed at rom bank 0x30 # samplenames = An empty array, or an array of sample names that will be used as savefiles with open(romfile, 'rb') as f: rom = f.read() projectdata_pos = musyx_position + snd_ProjectData - 0x4000 sampletable_address_pos = projectdata_pos + sdp_SampleTableAddress sampletable_offset = littledata_to_word(rom, sampletable_address_pos) sampletable_pos = sampletable_offset + projectdata_pos sample_n_pos = projectdata_pos + sdp_NumberOfSamples sample_n = rom[sample_n_pos] targetdir = "python/out/Samples/" os.makedirs(targetdir, exist_ok=True) for i in range(sample_n): if (i < len(samplenames)): name = "sample_{:03}_{}".format(transform_id(i, ID_SAMPLE), samplenames[i]) else: name = "sample_{:03}".format(transform_id(i, ID_SAMPLE)) print(name) sample_address = littledata_to_word(rom, sampletable_pos + i * 6 + 0) sample_length = littledata_to_word(rom, sampletable_pos + i * 6 + 2) sample_quality = rom[sampletable_pos + i * 6 + 4] sample_bank = rom[sampletable_pos + i * 6 + 5] sample_pos = musyx_position + sample_address - 0x4000 + sample_bank * 0x4000 f = wave.open(targetdir + name + ".wav", "wb") f.setparams((1, 2, 8192 if sample_quality else 1920, sample_length * 32, 'NONE', 'not compressed')) bytearraylist = [] datalength = 0 for row in range(sample_length): data = rom[sample_pos + row * 0x10:sample_pos + row * 0x10 + 0x10] firstdatum = get_sample_datum(data, 0) if (firstdatum == 0x70): reps = data[1] + 1 datalength += reps * 0x20 bytearraylist.append(bytearray([0x00, 0x00] * 0x20 * reps)) else: for j in range(0x20): datum = get_sample_datum(data, j) datum = (datum - 0x80) % 0x100 datalength += 1 bytearraylist.append(bytearray([0x00, datum])) f.writeframes(b''.join(bytearraylist))