Difference between revisions of "CSNG (File Format)"
(→Continuous Pitch / Modulation Data) |
(Multi-track SNG clarifications) |
||
Line 1: | Line 1: | ||
− | The '''CSNG format''' contains MIDI data. It appears in Metroid Prime 1 and 2. It is essentially MusyX's | + | 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. |
__TOC__ | __TOC__ | ||
Line 7: | Line 7: | ||
All offsets are relative to the start of the main header (after the custom header). | All offsets are relative to the start of the main header (after the custom header). | ||
− | Timings are represented in ''ticks'' | + | 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 <code>384 * 120 / 60 = 768</code> ticks-per-second). | ticks per beat (e.g. 120 beats-per-minute works out to <code>384 * 120 / 60 = 768</code> ticks-per-second). | ||
Line 37: | Line 42: | ||
| 0x10 | | 0x10 | ||
| 4 | | 4 | ||
− | | ''' | + | | '''SNG File Length''' |
|- | |- | ||
| 0x14 | | 0x14 | ||
Line 52: | Line 57: | ||
| 0x0 | | 0x0 | ||
| 4 | | 4 | ||
− | | ''' | + | | '''Track Index Offset'''; usually 0x18 for GCN games |
|- | |- | ||
| 0x4 | | 0x4 | ||
| 4 | | 4 | ||
− | | ''' | + | | '''Region Data Index Offset''' |
|- | |- | ||
| 0x8 | | 0x8 | ||
| 4 | | 4 | ||
− | | '''Channel Map Offset''' | + | | '''Channel Map Offset''' |
|- | |- | ||
| 0xC | | 0xC | ||
| 4 | | 4 | ||
− | | '''Tempo Table Offset'''; | + | | '''Tempo Table Offset'''; 0x0 if tempo doesn't change |
|- | |- | ||
| 0x10 | | 0x10 | ||
Line 75: | Line 80: | ||
|- | |- | ||
| 0x18 | | 0x18 | ||
− | |||
− | |||
− | |||
− | |||
| colspan=2 {{unknown|End of header}} | | colspan=2 {{unknown|End of header}} | ||
|} | |} | ||
− | === | + | ===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: | |
{| class="wikitable" | {| class="wikitable" | ||
Line 93: | Line 96: | ||
| 0x0 | | 0x0 | ||
| 4 | | 4 | ||
− | | '''Start Tick'''; time-point to begin executing | + | | '''Start Tick'''; time-point to begin executing region data |
|- | |- | ||
| 0x4 | | 0x4 | ||
Line 101: | Line 104: | ||
| 0x8 | | 0x8 | ||
| 2 | | 2 | ||
− | | ''' | + | | '''Region Data Index'''; used to select region data via the ''region data index''; value is negative on a dummy ''region info'' to terminate track regions |
|- | |- | ||
| 0xA | | 0xA | ||
| 2 | | 2 | ||
− | | ''' | + | | {{unknown|'''Unknown'''}} |
|- | |- | ||
| 0xC | | 0xC | ||
− | + | | colspan=2 {{unknown|End of region info}} | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | | colspan=2 {{unknown|End of | + | |
|} | |} | ||
− | === | + | ===Region Data=== |
− | Here begins a free-form blob of indexed | + | Here begins a free-form blob of indexed region data. It starts with a variable-length |
− | '''u32 array''' of | + | '''u32 array''' of SNG offsets for each region, then the region data itself. |
− | ==== | + | ====Region Data Header==== |
{| class="wikitable" | {| class="wikitable" | ||
Line 137: | Line 128: | ||
| 0x0 | | 0x0 | ||
| 4 | | 4 | ||
− | | ''' | + | | '''Region Data Header Size'''; size of the header ''after'' this field (always 0x8) |
|- | |- | ||
| 0x4 | | 0x4 | ||
| 4 | | 4 | ||
− | | '''Pitch Wheel Data Offset'''; (absolute | + | | '''Pitch Wheel Data Offset'''; (absolute SNG-offset) 0x0 if no pitch-wheel messages in region |
|- | |- | ||
| 0x8 | | 0x8 | ||
| 4 | | 4 | ||
− | | '''Mod Wheel Data Offset'''; (absolute | + | | '''Mod Wheel Data Offset'''; (absolute SNG-offset) 0x0 if no mod-wheel messages in region |
|- | |- | ||
| 0xC | | 0xC | ||
− | | colspan=2 {{unknown|End of | + | | colspan=2 {{unknown|End of region data}} |
|} | |} | ||
− | ==== | + | ====Region Commands==== |
− | After the | + | After the region data header, the actual playback commands begin. There are only 2 types of commands |
− | in | + | in SNG: ''note'' and ''control change''. |
=====Delta Time RLE===== | =====Delta Time RLE===== | ||
Line 187: | Line 178: | ||
this is a '''note command'''. | this is a '''note command'''. | ||
− | Unlike MIDI, which has separate commands for note-on/note-off, | + | 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. | to a note-on command, which is then able to track its own lifetime. | ||
Line 235: | Line 226: | ||
|} | |} | ||
− | =====End Of | + | =====End Of Region===== |
− | When the two bytes following the delta-time == 0xffff, this | + | When the two bytes following the delta-time == 0xffff, this region has no more commands. |
====Continuous Pitch / Modulation Data==== | ====Continuous Pitch / Modulation Data==== | ||
− | If the pitch or mod offsets in a | + | 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 | (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 absolute time and value, summing each consecutive update for the current time/values. | ||
Line 282: | Line 273: | ||
===Channel Map=== | ===Channel Map=== | ||
− | This is a simple '''u8 table''' mapping 64 | + | This is a simple '''u8 table''' mapping 64 SNG tracks to 16 MIDI channels for instrument selection via the [[AGSC (File Format)#MIDI Setup Entry|SongGroup MIDI-Setup]]. |
===Tempo Table=== | ===Tempo Table=== | ||
− | When the | + | When the SNG has a non-zero tempo table offset, this song features tempo changes. |
The change events are simple absolute-tick / BPM pairs. | The change events are simple absolute-tick / BPM pairs. | ||
Revision as of 14:13, 20 June 2016
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.
Contents
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 384 * 120 / 60 = 768
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.
Offset | Size | Description |
---|---|---|
0x0 | 4 | Magic; (always 0x2) |
0x4 | 4 | MIDI Setup ID |
0x8 | 4 | SongGroup ID |
0xC | 4 | AGSC ID |
0x10 | 4 | SNG File Length |
0x14 | MusyX data starts |
Header
Offset | Size | Description |
---|---|---|
0x0 | 4 | Track Index Offset; usually 0x18 for GCN games |
0x4 | 4 | Region Data Index Offset |
0x8 | 4 | Channel Map Offset |
0xC | 4 | Tempo Table Offset; 0x0 if tempo doesn't change |
0x10 | 4 | Initial Tempo; (commonly 0x78 or 120 beats per minute) |
0x14 | 4 | Unknown |
0x18 | End of header |
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:
Offset | Size | Description |
---|---|---|
0x0 | 4 | Start Tick; time-point to begin executing region data |
0x4 | 4 | Unknown; commonly 0xffff0000 |
0x8 | 2 | Region Data Index; used to select region data via the region data index; value is negative on a dummy region info to terminate track regions |
0xA | 2 | Unknown |
0xC | End of region info |
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 Data Header
Offset | Size | Description |
---|---|---|
0x0 | 4 | Region Data Header Size; size of the header after this field (always 0x8) |
0x4 | 4 | Pitch Wheel Data Offset; (absolute SNG-offset) 0x0 if no pitch-wheel messages in region |
0x8 | 4 | Mod Wheel Data Offset; (absolute SNG-offset) 0x0 if no mod-wheel messages in region |
0xC | End of region data |
Region Commands
After the region data header, the actual playback commands begin. There are only 2 types of commands in SNG: 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:
def DecodeDeltaTimeRLE(streamIn):
total = 0
while True:
term = streamIn.ReadU16()
if term == 0xffff:
total += 0xffff
dummy = streamIn.ReadU16()
continue
total += term
return total
Note Command
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, SNG attaches a note length value to a note-on command, which is then able to track its own lifetime.
Offset | Size | Description |
---|---|---|
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.
Offset | Size | Description |
---|---|---|
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 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.
def DecodeRLE(streamIn):
# high-bit shift-trigger RLE, limited to 2 bytes
term = streamIn.ReadU8()
total = term & 0x7f
if term & 0x80:
total = total * 256 + streamIn.ReadU8()
return total
def DecodeContinuousRLE(streamIn, isValue):
total = 0
while True:
# 1-2 byte RLE accumulated within continuable RLE
term = DecodeRLE(streamIn)
if term == 0x8000:
total += 0xffff
dummy = streamIn.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
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.
Offset | Size | Description |
---|---|---|
0x0 | 4 | Tick; absolute time-point to perform tempo change |
0x4 | 4 | Tempo; new tempo in BPM |
0x2 | End of tempo change |