CSNG (File Format)

The CSNG format contains MIDI data. It appears in Metroid Prime 1 and 2. It is essentially MusyX's GameCube SNG music format, with a custom header.

Format
All offsets are relative to the start of the main header (after the custom header).

Overall, SNG functions similar to a Type-1 (multi-track) MIDI file, with (up to) 64 tracks merging their events into 16 General-MIDI sequencer channels. In addition to the multi-track structure, SNG supports storing MIDI regions as indexed patterns, so songs with repetitive refrains can save on memory by referencing patterns more than once.

Timings are represented in ticks. Unlike MIDI, the tick-resolution is fixed at 384 ticks per beat (e.g. 120 beats-per-minute works out to  ticks-per-second).

Custom Header
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.

Region Info
The track index has offsets relating the first region info for each track.

There is a sequence of at least 2 region info structures populating each track, last one acting as terminator:

Region Data
Here begins a free-form blob of indexed region data. It starts with a variable-length u32 array of SNG offsets for each region, then the region data itself.

Region Commands
After the region data header, the actual playback commands begin. There are only 3 types of commands in SNG: note, control change, and program 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:

Note Command
When the two bytes following the delta-time != 0xffff, and the high-bits of both bytes are unset, this is a note command.

Unlike MIDI, which has separate commands for note-on/note-off, SNG attaches a note length value to a note-on command, which is then able to track its own lifetime.

Control Change Command
When the two bytes following the delta-time != 0xffff, and the high-bits of both bytes are set, this is a control change command.

See the standard MIDI control numbers for details.

Program Change Command
When the two bytes following the delta-time != 0xffff, and the high-bit of the first byte is set, this is a program change command.

See the standard MIDI program numbers for details.

End Of Region
When the two bytes following the delta-time == 0xffff, this region has no more commands.

Continuous Pitch / Modulation Data
If the pitch or mod offsets in a region 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.

Channel Map
This is a simple u8 table mapping 64 SNG tracks to 16 MIDI channels for instrument selection via the SongGroup MIDI-Setup.

Tempo Table
When the SNG has a non-zero tempo table offset, this song features tempo changes. The change events are simple absolute-tick / BPM pairs.