Difference between revisions of "CSNG (File Format)"

From Retro Modding Wiki
Jump to: navigation, search
(Continuous Pitch / Modulation Data)
Line 250: Line 250:
 
<syntaxhighlight lang="python" line="1">
 
<syntaxhighlight lang="python" line="1">
 
def DecodeRLE(in):
 
def DecodeRLE(in):
 +
    # high-bit shift-trigger RLE, limited to 2 bytes
 
     term = in.ReadU8()
 
     term = in.ReadU8()
 
     total = term & 0x7f
 
     total = term & 0x7f
Line 256: Line 257:
 
     return total
 
     return total
  
def DecodeContinuousRLE(in):
+
def DecodeContinuousRLE(in, isValue):
 
     total = 0
 
     total = 0
 
     while True:
 
     while True:
 +
        # 1-2 byte RLE accumulated within continuable RLE
 
         term = DecodeRLE(in)
 
         term = DecodeRLE(in)
 
         if term == 0x8000:
 
         if term == 0x8000:
Line 266: Line 268:
 
         total += term
 
         total += term
  
         if total >= 0x4000:
+
         # values are signed deltas;
            return total - 0xffff
+
        # extending into the high-half of 15-bit precision
        else:
+
        if isValue:
            return total
+
            if total >= 0x4000:
 +
                return total - 0xffff
 +
            else:
 +
                return total
 +
 
 +
        # times are always forward-deltas
 +
        return total
 
</syntaxhighlight>
 
</syntaxhighlight>
  

Revision as of 18:26, 19 May 2016

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.

Format

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).

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 SON File Length
0x14 MusyX data starts

Header

Offset Size Description
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)
0x14 4 Unknown
0x18 256 Track Header Offsets; (absolute SON-offsets) 64 elements, 0x0 if track not present
0x118 End of header

Track Header

This is a variable-length table of headers for each track

Offset Size Description
0x0 4 Start Tick; time-point to begin executing track data
0x4 4 Unknown; commonly 0xffff0000
0x8 2 Track Data Index
0xA 2 Padding
0xC 4 Start Tick; copy of start tick
0x10 4 Unknown; commonly 0xffff0000
0x14 4 Unknown; commonly 0xffff0000
0x18 End of header

Track Data

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

Offset Size Description
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

Track Commands

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:

  1. def DecodeDeltaTimeRLE(in):
  2.     total = 0
  3.     while True:
  4.         term = in.ReadU16()
  5.         if term == 0xffff:
  6.             total += 0xffff
  7.             dummy = in.ReadU16()
  8.             continue
  9.         total += term
  10.         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, SON 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 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.

  1. def DecodeRLE(in):
  2.     # high-bit shift-trigger RLE, limited to 2 bytes
  3.     term = in.ReadU8()
  4.     total = term & 0x7f
  5.     if term & 0x80:
  6.         total = total * 256 + in.ReadU8()
  7.     return total
  8.  
  9. def DecodeContinuousRLE(in, isValue):
  10.     total = 0
  11.     while True:
  12.         # 1-2 byte RLE accumulated within continuable RLE
  13.         term = DecodeRLE(in)
  14.         if term == 0x8000:
  15.             total += 0xffff
  16.             dummy = in.ReadU8()
  17.             continue
  18.         total += term
  19.  
  20.         # values are signed deltas;
  21.         # extending into the high-half of 15-bit precision
  22.         if isValue:
  23.             if total >= 0x4000:
  24.                 return total - 0xffff
  25.             else:
  26.                 return total
  27.  
  28.         # times are always forward-deltas
  29.         return total

Channel Map

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

Tempo Table

When the SON 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