CSNG (File Format)
The CSNG format contains MIDI data. It appears in Metroid Prime 1 and 2. It is essentially MusyX's SON music format, with a custom header.
All offsets are relative to the start of the main header (after the custom header).
Timings are represented in ticks, like MIDI. Unlike MIDI, the tick-resolution is fixed at 384
ticks per beat (e.g. 120 beats-per-minute works out to
384 * 120 / 60 = 768 ticks-per-second).
This 0x14-byte header isn't part of the MusyX format; it appears at the start of the file. After parsing this the rest of the file is copied into a buffer and then passed to the MusyX functions.
|0x0||4||Magic; (always 0x2)|
|0x4||4||MIDI Setup ID|
|0x10||4||SON File Length|
|0x14||MusyX data starts|
|0x0||4||Version; always 0x18|
|0x4||4||Track Data Offset; (absolute SON-offset)|
|0x8||4||Channel Map Offset; (absolute SON-offset)|
|0xC||4||Tempo Table Offset; (absolute SON-offset) 0x0 if tempo doesn't change|
|0x10||4||Initial Tempo; (commonly 0x78 or 120 beats per minute)|
|0x18||256||Track Header Offsets; (absolute SON-offsets) 64 elements, 0x0 if track not present|
|0x118||End of header|
This is a variable-length table of headers for each track
|0x0||4||Start Tick; time-point to begin executing track data|
|0x4||4||Unknown; commonly 0xffff0000|
|0x8||2||Track Data Index|
|0xC||4||Start Tick; copy of start tick|
|0x10||4||Unknown; commonly 0xffff0000|
|0x14||4||Unknown; commonly 0xffff0000|
|0x18||End of header|
Here begins a free-form blob of indexed track data. It starts with a variable-length u32 array of SON offsets for each track, then the track data itself.
Track Data Header
|0x0||4||Track Data Header Size; size of the header after this field (always 0x8)|
|0x4||4||Pitch Wheel Data Offset; (absolute SON-offset) 0x0 if no pitch-wheel messages on track|
|0x8||4||Mod Wheel Data Offset; (absolute SON-offset) 0x0 if no mod-wheel messages on track|
|0xC||End of header|
After the track data header, the actual playback commands begin. There are only 2 types of commands in SON: note and control change.
Delta Time RLE
Just like MIDI, each command starts with a delta time value telling the sequencer how many ticks to wait after the previous command. Unlike MIDI, Factor5 uses a custom RLE scheme to adaptively scale the value's precision to reduce the value's size.
The RLE operates on 16-bit words, with the value 0xffff triggering a continuation, then a 'dead' 16-bit word skipped over, then the 0xffff is summed with the following RLE value, looping the decode algorithm.
In Python, decoding works like so:
<syntaxhighlight lang="python" line="1"> def DecodeDeltaTimeRLE(in):
total = 0 while True: term = in.ReadU16() if term == 0xffff: total += 0xffff dummy = in.ReadU16() continue total += term return total
When the two bytes following the delta-time != 0xffff, and the high-bit of the first byte is unset, this is a note command.
Unlike MIDI, which has separate commands for note-on/note-off, SON attaches a note length value to a note-on command, which is then able to track its own lifetime.
|0x0||1||Note; AND with 0x7f for the value|
|0x1||1||Velocity; AND with 0x7f for the value|
|0x2||2||Note Length; count of ticks before note-off issued by sequencer|
|0x4||End of note|
Control Change Command
When the two bytes following the delta-time != 0xffff, and the high-bit of the first byte is set, this is a control change command.
See the standard MIDI control numbers for details.
|0x0||1||Value; AND with 0x7f for the value|
|0x1||1||Control; AND with 0x7f for the value|
|0x2||End of control change|
End Of Track
When the two bytes following the delta-time == 0xffff, this track has no more commands.
Continuous Pitch / Modulation Data
If the pitch or mod offsets in a track are non-zero, they point to a buffer of RLE-compressed (delta-tick, delta-value) pairs, decoding to signed 16-bit precision. The decoder must track the absolute time and value, summing each consecutive update for the current time/values.
The algorithm for this RLE is different than the delta-time one for commands. It may scale down to a single byte if able.
<syntaxhighlight lang="python" line="1"> def DecodeRLE(in):
# high-bit shift-trigger RLE, limited to 2 bytes term = in.ReadU8() total = term & 0x7f if term & 0x80: total = total * 256 + in.ReadU8() return total
def DecodeContinuousRLE(in, isValue):
total = 0 while True: # 1-2 byte RLE accumulated within continuable RLE term = DecodeRLE(in) if term == 0x8000: total += 0xffff dummy = in.ReadU8() continue total += term
# values are signed deltas; # extending into the high-half of 15-bit precision if isValue: if total >= 0x4000: return total - 0xffff else: return total
# times are always forward-deltas return total
This is a simple u8 table mapping 64 SON tracks to 16 MIDI channels for instrument selection via the SongGroup MIDI-Setup.
When the SON has a non-zero tempo table offset, this song features tempo changes. The change events are simple absolute-tick / BPM pairs.
|0x0||4||Tick; absolute time-point to perform tempo change|
|0x4||4||Tempo; new tempo in BPM|
|0x2||End of tempo change|