TABLE OF CONTENTS
0.0 Intro
1.0 MusyX Versions
2.0 Structure Overview
3.0 How MusyX Handles Sound
3.1 Raw sample
3.2 SoundMacros
4.0 Setting Up Output Files and the Format of Output Files
4.1 Generating .pool and .proj files from musician's project
5.0 .proj file format
5.1 SoundMacro_Table
5.2 ADSR_Table
5.3 SFX_Table
5.4 Sample_Table
5.5 Song_Table
6.0 .pool file format
6.1 Sample data
6.2 Song data
6.2.0 .song info
7.0 ADSR format (.mxt files)
8.0 SoundMacro format (.mxm files)
9.0 Python Package
9.1 Finding the MusyX data
9.2 Determining Song and SoundMacro count, and choosing gm2song.exe
9.3 Extracting the Data
10.0 Miscellaneous Notes or MusyX Design Considerations
--------------------
0.0 Intro
MusyX is a sound system developed by Factor 5 for the Gameboy and other consoles.
It essentially acts as a sound driver, simplifying the life of the musician and programmer. The musician provides midi files and programmed sound effects which are converted and assigned ids. These ids can be used by the programmer to play music and sound effects.
This document focuses on the structure of the Gameboy Color and Monochrome Gameboy MusyX sound driver. The driver is identical for both gameboy versions.
An archived version of the MusyX musician and programmer manual is available on archive.org: https://archive.org/details/MusyXAudioToolsForNintendo64AndGameBoy/page/n5/mode/2up
Please browse the manual to get a general understanding of how MusyX works. All public functions are documented there. This document assumes you have at least a superficial understanding of the following sections of the manual:
Introduction (p3)
Working with MusyX (p11)
Overview (p33)
SoundMacros and the SoundMacro Editor (p35)
The Project Manager (p69)
Walk Through (p75)
Additional Tools (p91)
Data Conversion (p99)
Appendix 2 - Game Boy Musicians Reference (p177)
Appendix 4 - Game Boy Programmers Reference (p345)
1.0 MusyX Versions
There are several released versions of MusyX. Examples include:
A) The MusyX installation cd .iso found somewhere on the internet:
MusyX.exe version 1.01
gm2song.exe version 1.03
MUConv.exe version 1.04
B) The MusyX version used by Orkiz in his publically released MusyX examples:
MusyX.exe version ?
gm2song.exe version 1.29g
MUCONV.exe version 1.15
There are definitely other versions of the sound driver as well.
This document is based on research from MusyX.exe 1.01 / gm2song.exe version 1.03 / MUConv.exe version 1.04
The document is also based on research from the version of the MusyX driver used in MagiNation which seems to be a version almost identical to (A)
2.0 Structure Overview
The MusyX driver is composed of several components. First, there is the driver code that varies from MusyX version to MusyX version. Secondly, there is generated sound data which is compiled from the musician's midi files and sound effect code.
Driver code:
1) musyx.o
This is the main code of the driver. It's about $1A79 bytes long (the exact size varies based on the driver version). It must be placed at the beginning of any bank, address $4000. This bank is called bank MUSYX
2) musyxb0.o
This contains driver code that needs to use the second ROM bank to access data. It is $550 bytes long and needs to be placed anywhere in bank 0. It is often placed at the end of bank 0 (address $3AB0 to $4000), but this is up to the discretion of the developer.
3) WRAM memory
MusyX reserves $DF00 - $DFFF of a WRAM bank to store information to operate the driver
Sound data:
1) game.proj (snd_ProjectData)
Located immediately after musyx.o (i.e. approximately at address $5A79 in bank MUSYX)
Contains lookup tables to all the sound data, as well as some sound data
2) game.pool (Pool Data)
Located at address $4000 of bank (MUSYX+1). Is usually multiple banks long. Contains Sample data, and Midi file data.
3.0 How MusyX Handles Sound
The gameboy version of MusyX ignores many parameters from the MusyX.exe program. If a parameter is not mentioned in this guide, it is very likely completely ignored.
There are two types of sound:
1) Raw samples played using the Wave Pattern RAM of Channel 3
2) SoundMacros
3.1 Raw sample
These are played using the functions snd_StartSample or snd_PlaySample
These functions are seldom used. The format of the samples are identical to that of SoundMacros (see below).
3.2 SoundMacros
SoundMacros are very short snippets of code that send commands the the gameboy's sound registers, generating a specific sound effect. The sound can range from a simple frequency to simulate a note to an elaborate explosion-type sound effect, or anything in-between. The sound can also be a sample. 99% of sounds via MusyX are likely generated via SoundMacros as opposed to raw samples.
Note that Keymaps and Layers are converted to SoundMacros in an as-yet undetermined fashion.
There are two ways of triggering a SoundMacro:
1) snd_StartSFX will trigger 1 target SoundMacro
2) snd_StartSong will play a midi song. Every single "note" played in the original midi file triggers one SoundMacro that handles what each note should sound like
The SoundMacro itself can choose which channel 1-4 that it will use. For example, a SoundMacro playing a sample must use channel 3, and a SoundMacro generating noise must use channel 4, but a SoundMacro just playing a simple frequency to simulate a note could choose any channel.
Since multiple SoundMacros can be triggered and all want to use the same channel, each SoundMacro has a priority from 0-255. Only the SoundMacro with the highest priority will play, even potentially interrupting a currently-playing sound. Lower-priority SoundMacros will simply not play.
This means that although a Song + SFX in total can trigger as many SoundMacros as you want, there will always only be a maximum of 4 SoundMacros playing at any given point in time, with the rest ignored.
4.0 Setting Up Output Files and the Format of Output Files
This section covers the format of game.proj and game.pool, and how to generate them
4.1 Generating .pool and .proj files from musician's project
These two files are created by MUConv.exe.
An easy way to setup MUConv.exe is to create a .bat file with the following code:
muconv -t GB -v Script.txt GAME.des >out.txt
out.txt will contain debugging data for the output file generation.
Before running the .bat file, make sure to setup your GAME.des file with the following contents:
[Pool]
GROUPS
[Samples]
GROUPS
[Project]
GROUPS
[OutputDirectory]
Output
[Name]
GAME
[Include]
SoundIDs.i
SoundIDs.i contains the id of all the SFX and Songs
GAME is the name of your output files
GROUPS is a list of all the subfolders under "Groups" in the MusyX project.
In many cases, GROUPS will equate to:
Songgroup\nSFXgroup
5.0 .proj file format
MUConv.exe is responsible for generating the .proj file.
The .proj file is appended to the same bank as musyx.o (bank MUSYX), right after musyx.o. Both musyx.o and the .proj file must fit within one $4000 bank.
All addresses are relative to the start of the .proj file. In other words, the .proj file is usually placed at address $5A79. An address of $0010 for example would point to $5A89 in the bank.
Banks are relative to bank MUSYX. For example, if musyx.o and the .proj files are placed in bank $20, then a bank of 3 would refer to the actual bank $23.
Offset
Name
Info
$0000
ADSR_TableAddress
Address of the ADSR lookup table
$0002
SFX_TableAddress
Address of the SFX lookup table
$0004
SFX_n
Total number of SFX
$0005
Sample_TableAddress
Address of the Sample lookup table
$0007
Sample_n
Total number of samples
$0008
db $00
This value is only non-zero when making a test GB slave ROM, in which case the value is equal to Number_of_Samples
$0009
SampleMap_SoundMacro_Address
Address of the start of the SoundMacro that contains SampleMap commands. dw $0000 if there is no such SoundMacro
$000B
SampleMap_n
Number of entries in the SampleMap, excluding the END command
$000C
Song_TableAddress
Address of the Song lookup table
$000E
Song_n
Total number of songs
$000F
SoundMacro_Table
See filestructure below
ADSR_TableAddress
ADSR_Table
See filestructure below
SFX_TableAddress
SFX_Table
See filestructure below
Sample_TableAddress
Sample_Table
See filestructure below (section 6.1)
Song_TableAddress
Song_Table
See filestructure below
EOF
5.1 SoundMacro_Table
All addresses are relative to the start of SoundMacro_Table. In other words, SoundMacro_Table is usually placed at address $5A79+$000F. An address of $0010 for example would point to $5A98 in the bank.
SoundMacro_Table has two sections:
1) A lookup table
A list of n addresses in sequential order pointing to the opcodes of each SoundMacro
e.g. dw $0006, $0015, $002A
2) Raw opcode data in sequential order of each SoundMacro
All n SoundMacros are written here sequentially. Each SoundMacro is converted from .mxm to a more compact data form. See below for more info. Each SoundMacro can be a maximum of 0x200 bytes after being converted to data
You can tell how long the lookup table is by taking address at index 0 and dividing it by 2, because this address points to the first SoundMacro which is placed immediately at the end of the lookup table.
5.2 ADSR_Table
This section is formatted in a similar manner to SoundMacro_Table
All addresses are relative to the start of ADSR_Table.
ADSR_Table has two sections:
1) A lookup table
A list of n addresses in sequential order pointing to raw ADSR data.
e.g. dw $0004, $000B
2) Raw ADSR data in sequential order of each ADSR
See below for the data format. Each ADSR data entry is always exactly 7 bytes long.
You can tell how long the lookup table is by taking address at index 0 and dividing it by 2.
5.3 SFX_Table
As a reminder, each SFX triggers exactly 1 SoundMacro
Each entry in this table is 4 bytes long (total length SFX_n*4)
db SoundMacro_Table_index - points to the linked SoundMacro
db Priority - Default priority of the SFX (0-255) to pass to the SoundMacro
db DefKey - Default MIDI key (0-127) to pass to the SoundMacro
db DefVel - Default velocity (i.e. volume)*
*The default velocity is converted from a number 0-127 using the following code:
IF Volume == 0
0
ELSE
max(1,Volume//8)
5.4 Sample_Table
Each entry in this table is 6 bytes long (total length Sample_n*6)
See the .pool data structure for more info on samples
dw AbsoluteAddress - points to the absolute address of the sample.
dw Size/$10 - Size of the sample data divided by $10
db HighQuality - 1 if high-quality sample, or else 0 if low-quality sample
db RelativeBank - bank of the sample minus bank MUSYX
5.5 Song_Table
Each entry in this table is 3 bytes long (total length Song_n*3)
db RelativeBank
dw AbsoluteAddress
6.0 .pool file format
MUConv.exe is responsible for generating the .pool file. gm2song.exe is executed by MUConv.exe to convert .mid files to .song files, which are inserted into the .pool file.
The .pool file starts in bank (MUSYX + 1) and can be multiple banks long.
The .pool file has two sections:
1) Sample data section
2) Song data section
6.1 Sample data
Sample data starts at bank (MUSYX+1). Even if there are no samples and there is therefore no sample data, this section takes up at least 1 bank. Empty space at the end of the last bank is uninitialized, meaning that other file fragments from the developer's computer might show up in the uninitialized space.
You can convert .aiff, .mort or .wav into samples.
Here's how you convert a .wav into a sample:
First, the .wav must be in 16-bit, mono format
If the .wav is 4000Hz+, MUConv.exe will assume that the .wav had a frequency of 8192 Hz. If the quality is 3999Hz-, it will assume the .wav had a frequency of 1920 Hz. Therefore, any value other than 1920Hz or 8192Hz will cause the sample to be played back at the wrong speed.
Note that even though the MusyX manual mentions low-quality, medium-quality and high-quality samples, there are actually only 2 speeds: low (1920 Hz) and medium/high (8192 Hz). A high-quality sample is considered medium-quality if it is played via a SoundMacro or snd_StartSample, and is considered high-quality if it is played via snd_PlaySample.
Since the .wav is in a 16-bit mono format, each input datum is a 16-bit word, where $0000 is neutral, $7FFF is the highest positive value, and $8000 is the lowest negative value.
The output datum that is stored in .pool is a 4-bit (one nibble) datum:
datum = datum >> 12 ;take the 4 most significant bits
datum = (datum + 8) % 16
The output datum are sequentially ordered. If the total number of data are not evenly divisable by 32, The data is padded with nibbles of $7 until it is evenly divisable. (i.e. each block of $10 bytes contains 32 data points) (See Pan Docs info on the registers FF30-FF3F Wave Pattern RAM). The length of a sample is always a multiple of $10.
There is one special exception the above rules. The very first datum of a 32-data block contains special info: if the first datum is $7, it indicates compressed silent data.
Therefore, if the very first datum is supposed to evaluate to $7, it is changed to $8 to avoid indicating compressed data.
If MUConv.exe comes across two or more sequential 32-data blocks in a row that evaluate to [$88]*16 (i.e. multiple blocks of 32*$0000 in the original .wav file), then the special compressed silent data format will be set.
The special compression format is:
[$7?] + [$NN] + [$??]*14,
where the data in ? is ignored
and where ($NN + 1) indicates the number of times to write the silent sample. $NN's maximum value is $FE.
Practically speaking, MUConv.exe encodes the data as:
[$78] + [$NN] + [$88]*14
The MusyX driver will copy the raw contents of each sample block directly into the Wave Pattern RAM. However, if it comes across compressed silent data, it will fill the Wave RAM with [$88]*16 for ($NN + 1) times. The only exception is the function snd_PlaySample, where the Wave Pattern RAM is filled with [$77]*16 instead.
Note that samples can cross over from the end of one bank into the next bank.
6.2 Song data
The song data section is placed at the bank immediately after the sample data. This means that if the sample data takes up 1 bank or less, the song data will start at bank (MUSYX+2).
A song must fit within a single bank, and cannot cross into another bank. However, MUConv.exe will fit multiple songs into the same bank if there is space.
Each song comes with a header and raw song data.
The header is created by MUConv.exe. The header is always $84 bytes long
This section assumes you have a superficial understanding of the .midi file format (see http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html or https://jimmenard.com/midi_ref.html)
The first 4 bytes indicate the default Program of channels 1-4. The default Program for the first 4 channels are defined in The MIDI Setup Window (page 93 of the MusyX manual) as "Prg.". The first 4 bytes have a value of (0-127), corresponding to the 128 possible Program choices.
The next $80 bytes map all 128 Programs to SoundMacros (Soundlist in the midi setup of MusyX.exe). Essentially, each byte is a pointer to the id of the target SoundMacro.
Following the header immediately comes the raw song data.
The raw song data (.song) is generated by gm2song.exe
6.2.0 .song info
This is how gm2song.exe interprets a midi file and generates a .song file. Note that all addresses are relative to the start of the .song file
All meta events ($FF) except End of Track and Set Tempo are ignored
System messages ($F0/F2/F3) are ignored. Other System messages > $F0 are unsupported and give warnings
If a note starts and stops on the same tick (same pitch and channel), the program will assume they are inverted to fix the presumed error
e.g. Time 0 Start Note
Time 10 Start Note --\
Time 10 End Note --/ These two notes will be swapped
Time 20 End Note
For GBC, the 4 tracks are defined by Channels 0-3. Other channels are discarded...?
BPM defaults to 120 if undefined
The earliest defined BPM is used. Tempo changes are not supported
The division of the midi file is always converted to 24 ticks. All references to "time" below refer to number of ticks, where 24 ticks = 1 quarter note.
The maximum distance between notes is 4096 ticks, or 170 quarter notes.
When adding a program change event, it will only be executed at the time of the subsequent note, so it always assumes a program change event will occur simultaneously with a note.
NOTE THAT ALL 2-BYTE VALUES ARE BIG-ENDIAN
File header
ds 2 - pointer to Track table (always $0006)
ds 2 - pointer to Pattern table
ds 2 - BPM
Track table
ds 2 - pointer to Track 0 header, or 0 if undefined
ds 2 - pointer to Track 1 header, or 0 if undefined
ds 2 - pointer to Track 2 header, or 0 if undefined
ds 2 - pointer to Track 3 header, or 0 if undefined
Track
null if track is unused
Each track defines 1 or 2 normal patterns (PATTERNA)
Each track ends with a pattern that contains loop information (PATTERNB)
PATTERNA
First PATTERNA defines a pattern from start of the song until the beginning of the song loop
ds 2 - time to first event
ds 1 - pattern index number
PATTERNA
Second PATTERNA defines a pattern from when the loop starts
This block only exists if (1) there is a song loop and (2) the song doesn't loop back to the very beginning, but somewhere in the middle
ds 2 - time to first event after song loop, minus the time of the first PATTERNA
ds 1 - pattern index number
PATTERNB
If there is a loop:
ds 2 - time between the first event of the previous PATTERNA until LoopEndRepeat
db $FE
ds 2 - The offset from the start of the header pointing to the beginning of the last PATTERNA (i.e. a value of 3*(count(PATTERNA)-1))
ds 2 - If the Loop Start is in front or simultaneous with the first event, return the delta time between these two (0 if simultaneous)
If there is no loop:
dw $0000
db $FF
dw $0000
dw $0000
Overall, this can be generalized as:
PATTERN
ds 2 - time between the previous block and the start of this block
ds 1 - pattern index number, $FE if loop, $FF if no loop
(ds 2 - If $FE/$FF, track header offset for loop)
(ds 2 - If $FE/$FF, time between the current block and the start of the block that's at the loop start)
Pattern table
ds 2*number_of_patterns - pointers to Pattern x
There are up to 8 patterns. Each BLOCKA references a new pattern
A pattern acts as a mini track. A track is made up of 1-2 patterns.
In other words, a song that loops back to the middle of the song is split
There are up to 4 patterns to represent the music before the loop, and up to 4 patterns defining the music during the loop
If there are 4 defined track headers each with a single BLOCKA,
then there are 4 patterns
If there are only 3 defined track headers each with a single BLOCKA,
some versions of gm2song.exe will still define 4 patterns (an empty pattern will be inserted)
If there are 4 defined track headers with two BLOCKAs,
then there are 8 patterns
Pattern data
Each entry in the pattern table has a pattern.
Each pattern will have X events (notes to play), a possible ending ProgramChangeEvent, and an end of pattern
At the very minimum, a null pattern has only the end of pattern bytes (3 bytes)
Pattern schema:
(Event)*X
(Possible ProgramChangeEvent)
$F0 $00 $FF = EndOfPatternEvent
Event (4-6 bytes long):
1) ds 2
Bit 15-12 = Velocity (always 1-F)
Velocity is calculated as (Velocity + 4)/8, capped for a range 1-F
Bit 11-0 = DeltaTime from last event's NoteOn (Max delta $FFF ticks)
2) ds 1
Bit 7 = Flag saying that (3) is defined (extra program command)
Bit 6-0 = NoteOn key
3) ds 1 - only defined if Bit 7 from (2) was set to 1, or else this byte is skipped
4) Duration of note (time until NoteOff)
If bit 7 of the first byte is 1, then
ds 2: A length defined by bits 14-0 (15 bits long)
Else
ds 1: A length defined by bits 6-0 (7 bits long)
ProgramChangeEvent:
Usually program changes are merged with the next note. However, if the program change event occurs after the very last note, it shows up by itself as follows:
1) ds 2
Bit 15-12 = Velocity 0 (only time the velocity can be 0)
Bit 11-0 = DeltaTime from the last NoteOn
2) ds 1
Bit 7: 1
Bit 6-0: Program data
EndOfPatternEvent:
1) ds 2
Bit 15-12 = Velocity (must be $0F)
Bit 11-0 = DeltaTime (always $000 but doesn't necessarily have to be so)
2) ds 1 - $FF
If the velocity is not $0F, the event will instead be interpreted as an Event with an extra program command. I'm not sure how MusyX would handle an event with note 127 with a velocity of $0F (I'm guessing it would decrement the velocity or doa separate ProgramChangeEvent+Event)
7.0 ADSR format (.mxt files)
The source .mxt file is little-endian and contains the following 8 bytes of data:
0:2 Attack time (milliseconds)
2:4 Decay time (milliseconds)
4:6 Sustain level ($1600 = 100.00%)
6:8 Release time (milliseconds)
The output data is little-endian and is 7 bytes long:
v = min($0F,math.floor(Sustain*0.003662109375+0.5)) ;about 1/273
dw math.floor($0F*$100/cycles(max(1,Attack))+0.5)
dw -(($0F-v)*$100//cycles(max(1,Decay)))
db v
dw -(v*$100//cycles(max(1,Release)))
Note that the "max" velocity is $1000 and not $1600, even though MusyX.exe says that $1600 is 100%. You should always base your Sustain level as fraction of $1000 (72.7272%).
8.0 SoundMacro format (.mxm files)
Each SoundMacro can be a maximum of $200 bytes after being converted to data
Note that the endianness switches
.mxm files are little-endian
The lookup table in the .proj file is is little-endian
The SoundMacro opcode/raw data is BIG-ENDIAN
.mxm files conversion instructions
The .mxm format is a set of n instructions, where each instruction is 8 bytes long
In MusyX.exe, the values of all the bytes are labelled so it's easy to see how the data is represented
Alternatively, gameboy_macrodef.mxd included in MusyX indicates all the opcodes and the definition of each byte for each instruction
Finally, the MusyX.pdf guide details each instruction's parameters in detail
Before explaining every opcode one-by-one, here are some functions used in the conversion program that I will define
id(SoundMacroID or ADSRTableID or SampleID)
Given the development files' ID, return the index id of the Macro/ADSR/Sample in the output file (for samples, see the documentation on SAMPLEMAP)
address(SoundMacroID,SoundMacroStep)
Given the developer's MacroID + SoundMacroStep, return the address to jump to to continue reading (address $0000 points to SoundMacro_Table)
relative(SoundMacroLoopStep)
Returns the number of bytes from the END of the command to the start of the target command. Negative numbers and positive numbers are both supported
cycles(Milliseconds)
Converts Milliseconds to GB cycles (approx 60 per second)
If Milliseconds == 0, return 0
Else
x = double(Milliseconds)
return floor((x+16.6666660308837890625)*0.060000002384185791015625)
keycents(Key,Cents)
Converts Key and Cents into a word, where the upper is the Key and the lower is the Cents normalized from 0-99 to 0-255
First, normalize Key and Cents to be the same sign:
x = Key*100+Cents
Key2,Cents2 are extracted to have the same sign
e.g. if x = -356, then Key = -3 and Cents = -56
Cents2 *= 2.575757503509521484375 ; i.e. 255/99
return signedbyte(Key2)<<8 + signedbyte(Cents2)
OPCODE(Byte0) NAME(gameboy_macrodef.mxd)
StartByte:EndByte ValueName
db OutputByte
dw OutputWord
00 END
db $00
01 STOP
db $23
02 SPLITKEY
1:2 KeyNumber
2:4 SoundMacroID
4:6 SoundMacroStep
db $15
db KeyNumber
dw address(SoundMacroID,SoundMacroStep)
03 SPLITVEL
1:2 Velocity
2:4 SoundMacroID
4:6 SoundMacroStep
db $17
db Velocity
dw address(SoundMacroID,SoundMacroStep)
04 RESET_MOD
db $20
05 LOOP
(Can only loop backwards)
4:6 SoundMacroLoopStep
6:7 Times
db $05
db Times
dw relative(SoundMacroLoopStep)
06 GOTO
2:4 SoundMacroID
4:6 SoundMacroStep
db $06
dw address(SoundMacroID,SoundMacroStep)
07 WAIT
1:2 KeyOff
2:3 RandomTimeBool
6:8 Milliseconds ($FFFF = infinite wait)
db $04
db KeyOff
db RandomTimeBool
dw IF Milliseconds != $FFFF
cycles(Milliseconds)+1
IF RandomTimeBool and Milliseconds == $FFFF
$0001 ;Looks like infinite wait is not supported if RandomTimeBool is on?
IF not(RandomTimeBool) and Milliseconds == $FFFF
$0000
08 PLAYMACRO
1:2 Voice ($FF "keeps the voice chosen by the midi sequence")
2:4 SoundMacroID
4:5 DontResetBool
db $26
db IF Voice <= 3
x = Voice
ELSE
x = $FF
x | (DontResetBool*$80)
db id(SoundMacroID)
09 PLAYKEYSAMPLE
db $22
0A STOP_MOD
db $1F
0B SETVOICE
(Error if this is not the first command of a macro)
1:2 Voice
4:5 DontResetBool
5:6 ToggleBool
db $0E
db SWITCH Voice,DontResetBool,ToggleBool:
case 0-3,_,_ Voice | (DontResetBool*$80) | (ToggleBool*$10)
?case $FF,_,_ $FF (not sure, this might not happen)
case _,_,0 $FF
case _,_,1 (Voice & 3)| (DontResetBool*$80) | (ToggleBool*$10)
0C SETADSR
1:3 ADSRTableID
db $0F
db id(ADSRTableID)
0D SETVOLUME
1:2 Volume
db $09
db IF Volume == 0
0
ELSE
max(1,Volume//8)
0E PANNING
1:2 Panning
db $0A
db switch Panning:
case 0-$29 0 (represents 0)
case $2A-$54 1 (represents 64)
case $54-$FF 2 (represents 127)
0F ENVELOPE
1:2 AscendingBool
6:8 Milliseconds
IF AscendingBool
db $27
dw x = cycles(max(Milliseconds,1))
max(1,$0F00//x)
IF !AscendingBool
db $11
dw x = cycles(max(Milliseconds,1))
max(1,$0F00//x)*(-1)
10 STARTSAMPLE
1:3 SampleID
db $21
db id(SampleID)
11 VOICE_OFF
db $0D
12 KEYOFF
1:2 Voice
db $12
db Voice if Voice <= 3 else $FF
13 SPLITRND
1:2 Rand
2:4 SoundMacroID
4:6 SoundMacroStep
db $16
db Rand
dw address(SoundMacroID,SoundMacroStep)
14 VOICE_ON
(Same OpCode as WAVE_ON - VOICE_ON is used for Voice 1,2,4)
1:2 DutyCycle
db $0C
db DutyCycle if (DutyCycle <= 3 or DutyCycle == $FF) else $00
15 SETNOISE
1:2 PolyClock 0-13
2:3 PolyStep 0-1
3:4 FreqRatio 0-7
db $10
db PolyClock*%10000 + PolyStep*%1000 + FreqRatio
16 PORTLAST
1:2 Keys (signed)
2:3 Cents (signed)
6:8 Milliseconds
db $1B
dw keycents(Key,Cents)
dw cycles(max(Milliseconds,1))
17 RNDNOTE
1:2 NoteLow
2:3 Detune
3:4 NoteHigh
4:5 FreeBool
5:6 AbsBool
IF NoteLow == NoteHigh and NoteHigh == 127
NoteLow -= 1
ELSEIF NoteLow == NoteHigh and NoteHigh != 127
NoteHigh += 1
db $0B
db (FreeBool << 7) + AbsBool
db NoteLow
db NoteHigh
db keycents(0,Detune) ;Lower 8 bits
18 ADDNOTE
1:2 Add (signed)
2:3 Detune (signed)
3:4 TempKeyBool
db $08
db TempKeyBool
db Add
db keycents(0,Detune) ;Lower 8 bits
19 SETNOTE
1:2 Key
2:3 Detune (signed)
db $07
db Key
db keycents(0,Detune) ;Lower 8 bits
1A LASTNOTE
1:2 Add (signed)
2:3 Detune (signed)
db $14
db Add
db keycents(0,Detune) ;Lower 8 bits
1B PORTAMENTO
1:2 Key (signed)
2:3 Cents (signed)
3:4 RelBool
6:8 Milliseconds
db $02
dw keycents(Key,Cents)
dw cycles(max(Milliseconds,1))
db RelBool
1C VIBRATO
1:2 Keys (signed)
2:3 Cents (signed)
6:8 Milliseconds
x = min(cycles(max(Milliseconds//2,1)),$FF)
y = keycents(Key,Cents)
db $01
dw y//x
db x
1D PITCHSWEEP
1:2 NoteLimit (signed)
2:3 CentLimit (signed)
5:6 SweepBool
6:8 Milliseconds
x = min(cycles(max(Milliseconds,1)),$FF)
y = keycents(NoteLimit,CentLimit)
db $03
dw y//x
dw y
db SweepBool*$80
1E HARDENVELOPE
1:2 FadeInBool
6:8 Milliseconds
db $13
db FadeInBool*0b1000 + max(1,min(7,floor(Milliseconds*0.0042666667141020298004150390625 + 0.5)))
1F PWN_START
1:2 LowLimit
2:3 HighLimit
3:5 Speed
If LowLimit > HighLimit the values are swapped to fix the error
db $1D
db LowLimit
db HighLimit
dw 0 if cycles(Speed) == 0, else ((HighLimit - LowLimit) << 8)//cycles(Speed)
20 PWN_UPDATE
1:2 LowLimit
2:3 HighLimit
3:5 Speed
If LowLimit > HighLimit the values are swapped to fix the error
db $1E
db LowLimit
db HighLimit
dw 0 if cycles(Speed) == 0, else ((HighLimit - LowLimit) << 8)//cycles(Speed)
21 PWN_FIXED
1:2 Duty
db $24
db Duty
22 PWN_VELOCITY
db $25
23 SENDFLAG
1:2 FlagBit
db $1A
db FlagBit & 7
24 SAMPLEMAP
(max 127 entries)
(error if a non-SAMPLEMAP command precedes a SAMPLEMAP command)
1:3 SampleId
;There's no opcode header
db id(SampleID)
25 CURRENTVOL
1:2 Volume
db $28
db IF Volume == 0
0
ELSE
max(1,Volume//8)
26 WAVE_ON
(Same OpCode as VOICE_ON - WAVE_ON is used for Voice 3)
1:3 SampleID
db $0C
db id(SampleID) if SampleID != 0 else $FF
27 ADD_SET_PRIO
1:2 Priority (unsigned if SetBool else signed)
2:3 SetBool
db $29
db SetBool
db Priority
28 TRAP_KEYOFF
2:4 SoundMacroID
4:6 SoundMacroStep
db $18
dw address(SoundMacroID,SoundMacroStep)
29 UNTRAP_KEYOFF
db $19
9.0 Python Package
The python package is able to convert data from a rom file into the original contents of a MusyX.exe project. You can then convert the MusyX.exe project back into the original rom file data with a little bit of work.
The python package extracts the following:
1) SoundMacros
2) Midi files and parameters
3) ADSR
4) Samples
5) SFX parameters
This information is extracted in python/out/
The package was designed with MUConv.exe version 1.04 in mind, but it might be useful or compatible with other MUConv.exe versions.
9.1 Finding the MusyX data
First, you need to find your MusyX bank. Here are some somewhat unique byte strings that probably exist in all versions of MusyX:
1) $10 $11 $01 $20 $22 $02 $40 $44 $04 $80 $88 $08
2) $01 $23 $45 $67 $78 $9A $BC $DE
Search for one of these byte strings and use that bank as your MusyX bank
Alternatively, find the bank that makes multiple read/writes to WRAM in the $DF00-DFFE memory range, or the bank that makes multiple read/writes to the sound registers.
Next, you need to find where the MusyX driver code ends and where the project data starts. This should be located around address $5A79 but it varies by a few bytes depending on the MusyX version. In general, the MusyX driver ends with the following byte string: $AC $DF $AF $C9 $FC $F3 $CF $3F. The address immediately after $3F is the project data. Modify constants.py to insert that address.
9.2 Determining Song and SoundMacro count, and choosing gm2song.exe
To properly label the songs and soundmacros, the total number of songs and soundmacros must be known.
To do this, use example_setup.py
Set your MusyX bank and rom filepath.
Run the program and it should give you a number for the number of songs and soundmacros.
Edit musyx/utils.py and input these numbers.
There are two provided files, gm2song103.exe and gm2song129g.exe. You need to choose the version of this exe file that converts midi->data. If you aren't sure, you can try version 1.03 first. Simply rename gm2song103.exe to gm2song.exe so that your python program can find it.
9.3 Extracting the Data
After editing musyx.utils.py as described in 9.0.2, use example_parse.py to extract all the data from the rom.
By default, extract files will be labelled filetype_number.ext. However, if you provide a custom name, the files will be labelled filetype_number_label.ext
The custom names are defined in the arrays, where the index of the array corresponds to the index used in the rom file.
SOUNDMACROS
.mxm files will automatically be generated. Simply import these files in the indicated order.
For debug purposes, a file called macrodebug.txt is generated that labels the original data contents of the rom.
MIDI
Files called soundlist_XX.txt will be created. Each soundlist.txt represents a unique Songgroup. In MusyX.exe, you need to create a Songgroup folder in the correct order for each soundlist.txt. You must then open each Songgroup set all 128 of the Soundlist > Object IDs to match the information provided in the .txt file. Then, for each midi file, you must double-click on the Midisetup and change the Prg. of channels 1-4 to match the information provided in the .txt file.
The program will convert the .mid file back into data and compare to see if the conversion was perfect. It therefore needs gm2song.exe to be in the directory of the python project to run the conversion of the .mid file into data.
ADSR
.mxt files will automatically be generated. Simply import these files in the indicated order.
SAMPLES
.wav files will automatically be generated. Import these files in the indicated order.
I was not able to figure out the exact algorithm for determining the right order of the samples. Therefore, you might have to experimentally change the sample ids in order to get the sample order as in the original rom file. To do this, right-click a sample and select Properties, and then change the ObjectID (select Yes to updating the references to keep the SoundMacros in sync with the samples). Keep changing the ObjectIDs until you get the desired output order. To make sure you have the correct output order, when generating your .proj and .pool files, your debug out.txt at the very end of the file should be including the .wav files in decrementing order (004, 003, 002, 001).
SFX parameters
This will generate a file called sfxinfo.txt that will tell you what parameters to put in each for SFX. Simply create an SFXgroup and then transcribe the info from the .txt file into the SFXgroup
10.0 Miscellaneous Notes or MusyX Design Considerations
Here are some things I noted when decompiling the driver.
snd_Init may also take c as an undocumented input. Every time MusyX changes the bank, it will ldh [$FF00+c], BANK. You can use this to store the current bank in HRAM. Otherwise, make sure c points to an unimportant memory location before calling snd_Init
Each SoundMacro should ideally start with SETVOICE to better define its use. This is very important for SFXs which default to voice 0, but less important for SoundMacros generated exclusively from song notes, where the Track implies the default voice
The minimum and maximum key frequencies allowed are $24-$77 (C2-B8)
Key frequencies out of this range will wrap around
Volume is calculated from 4 parameters:
a = volume from SetSFXVolume or SetSongVolume
b = volume from SoundMacro (the velocity of a note in a song, or the manually defined volume in the SoundMacro file)
c = volume from envelope effect (ADSR, ENVELOPE)
d = volume from NR50 (hardware master volume)
Software volume = a*b*c
The function SPLITVEL will split based on the value of a*b
If using ADSRs, note that the "max" velocity is $1000 and not $1600, even though MusyX.exe says that $1600 is 100%. You should always base your Sustain level as fraction of $1000 (a fraction of 72.7272%).
The version of MusyX packaged with Magi-Nation (which has a version slightly later than MusyX.exe 1.01 / gm2song.exe version 1.03 / MUConv.exe version 1.04) has the following bugs:
RNDNOTE seems to be bugged for the setting rel/abs = 1. In addition, the note's sound effect might not update if used for any voice other than the first.
snd_SetMasterVolume is bugged. To set a volume, you can input a number $80-$87 instead of $00-$07 to bypass the bug
Random numbers for RNDNOTE (free) and WAIT are bugged:
For RNDNOTE, the Detune parameter will be ignored
For WAIT, for numbers higher than 4267 ms, the upper limit of the random number will be floored by 4267 ms
PORTAMENTO's relative flag seems bugged. Always use absolute. If you really need relative, PORTLAST might do the job for you instead
ADD_SET_PRIO is potentially bugged for negative relative numbers
Sample playback notes:
When using STARTSAMPLE on a low quality sound, MusyX will playback the sample at
about 48 Hz, and resync the sample every 60 Hz.
In other words, the first 26 data will be played, and then 6 data will be skipped
There can be a bit of misalignment due to small timing errors where 20-32 data will be played before jumping to the next block of 32 data.
You can significantly improve the quality of low-quality sample playback by fixing this. The first option is to edit the MusyX driver (see source code of function _snd_SetLowQSampleFrequency). The second option is to edit your wave file. First, resample your wave file to 1560 Hz. Next, stretch out the wave file as follows:
(1) Copy 26 data
(2) Write 6 data twice in a row
(3) Copy 20 data
(4) Write 6 data twice in a row
(5) Copy 20 data
etc
until end of file
Song playback speed
Songs are played back at 99.161132812% of the stated bpm value
-
Notifications
You must be signed in to change notification settings - Fork 0
GauChoob/musyx
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
 |  | |||
 |  | |||
 |  | |||
 |  | |||
Repository files navigation
About
A guide to the structure of MusyX data files used in DMG and CGB games. Also contains a python script to convert the data back into project files.
Resources
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published