CSNG (File Format): Difference between revisions

Jump to navigation Jump to search
imported>Jackoalan
imported>Jackoalan
 
(11 intermediate revisions by the same user not shown)
Line 1: Line 1:
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.  
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'', like MIDI. Unlike MIDI, the tick-resolution is fixed at 384
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
| '''SON File Length'''
| '''SNG File Length'''
|-
|-
| 0x14
| 0x14
Line 52: Line 57:
| 0x0
| 0x0
| 4
| 4
| '''Version'''; always 0x18
| '''Track Index Offset'''; usually 0x18 for GCN games
|-
|-
| 0x4
| 0x4
| 4
| 4
| '''Track Data Offset'''; (absolute SON-offset)
| '''Region Data Index Offset'''
|-
|-
| 0x8
| 0x8
| 4
| 4
| '''Channel Map Offset'''; (absolute SON-offset)
| '''Channel Map Offset'''
|-
|-
| 0xC
| 0xC
| 4
| 4
| '''Tempo Table Offset'''; (absolute SON-offset) 0x0 if tempo doesn't change
| '''Tempo Table Offset'''; 0x0 if tempo doesn't change
|-
|-
| 0x10
| 0x10
Line 75: Line 80:
|-
|-
| 0x18
| 0x18
| 256
| '''Track Header Offsets'''; (absolute SON-offsets) 64 elements, 0x0 if track not present
|-
| 0x118
| colspan=2 {{unknown|End of header}}
| colspan=2 {{unknown|End of header}}
|}
|}


===Track Header===
===Region Info===
 
The ''track index'' has offsets relating the first '''region info''' for each track.


This is a variable-length table of headers 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 track data  
| '''Start Tick'''; time-point to begin executing region data  
|-
|-
| 0x4
| 0x4
Line 101: Line 104:
| 0x8
| 0x8
| 2
| 2
| '''Track Data Index'''
| '''Region Data Index'''; used to select region data when positive; -1 value terminates track; -2 value loops jumps to ''Loop Region Index'' at this region's tick
|-
|-
| 0xA
| 0xA
| 2
| 2
| '''Padding'''
| '''Loop Region Index'''; index of region to jump to if ''Region Data Index'' set to -2
|-
|-
| 0xC
| 0xC
| 4
| colspan=2 {{unknown|End of region info}}
| '''Start Tick'''; copy of start tick
|-
| 0x10
| 4
| {{unknown|'''Unknown'''}}; commonly 0xffff0000
|-
| 0x14
| 4
| {{unknown|'''Unknown'''}}; commonly 0xffff0000
|-
| 0x18
| colspan=2 {{unknown|End of header}}
|}
|}


===Track Data===
===Region Data===


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


====Track Data Header====
====Region Data Header====


{| class="wikitable"
{| class="wikitable"
Line 137: Line 128:
| 0x0
| 0x0
| 4
| 4
| '''Track Data Header Size'''; size of the header ''after'' this field (always 0x8)
| '''Region Data Header Size'''; size of the header ''after'' this field (always 0x8)
|-
|-
| 0x4
| 0x4
| 4
| 4
| '''Pitch Wheel Data Offset'''; (absolute SON-offset) 0x0 if no pitch-wheel messages on track
| '''Pitch Wheel Data Offset'''; (absolute SNG-offset) 0x0 if no pitch-wheel messages in region
|-
|-
| 0x8
| 0x8
| 4
| 4
| '''Mod Wheel Data Offset'''; (absolute SON-offset) 0x0 if no mod-wheel messages on track
| '''Mod Wheel Data Offset'''; (absolute SNG-offset) 0x0 if no mod-wheel messages in region
|-
|-
| 0xC
| 0xC
| colspan=2 {{unknown|End of header}}
| colspan=2 {{unknown|End of region data}}
|}
|}


====Track Commands====
====Region Commands====


After the track data header, the actual playback commands begin. There are only 2 types of commands
After the region data header, the actual playback commands begin. There are only 3 types of commands
in SON: ''note'' and ''control change''.
in SNG: ''note'', ''control change'', and ''program change''.


=====Delta Time RLE=====
=====Delta Time Encoding=====


Just like MIDI, each command starts with a '''delta time''' value telling the sequencer  
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
how many ticks to wait after the previous command. Unlike MIDI, Factor5 uses a fixed 16-bit
[[wikipedia:Run-length encoding|RLE scheme]] to adaptively scale the value's precision
value to store the delta times. In case of a delta time greater than 65535, no-op commands
to reduce the value's size.
with a 65535 delta time are inserted into the command stream until the target time is attained.


The RLE operates on 16-bit words, with the value 0xffff triggering a continuation,
In a theoretical Python streaming API, the delta time can be decoded with no-ops automatically consumed like so:
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">
<syntaxhighlight lang="python" line="1">
def DecodeDeltaTimeRLE(in):
def DecodeDeltaTime(streamIn):
     total = 0
     total = 0
     while True:
     while True:
         term = in.ReadU16()
         term = streamIn.ReadU16()
         if term == 0xffff:
        next = streamIn.PeekU16()
         if next == 0:
            # Automatically consume no-op and continue accumulating time value
             total += 0xffff
             total += 0xffff
             dummy = in.ReadU16()
             streamIn.seekAhead(2)
             continue
             continue
         total += term
         total += term
         return total
         return total
</syntaxhighlight>
</syntaxhighlight>
=====No-Op Command=====
When the two bytes following the delta-time are both zero, this is a no-op command.
As mentioned before, no-ops are useful for accumulating delta times greater than 65535.


=====Note Command=====
=====Note Command=====


When the two bytes following the delta-time != 0xffff, and the high-bit of the first byte is ''set'',
When the two bytes following the delta-time != 0xffff, and the high-bits of both bytes are ''unset'',
this is a '''note command'''.  
this is a '''note command'''.  


Unlike MIDI, which has separate commands for note-on/note-off, SON attaches a ''note length'' value
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 213: Line 208:
=====Control Change Command=====
=====Control Change Command=====


When the two bytes following the delta-time != 0xffff, and the high-bit of the first byte is ''unset'',
When the two bytes following the delta-time != 0xffff, and the high-bits of both bytes are ''set'',
this is a '''control change command'''.
this is a '''control change command'''.


Line 235: Line 230:
|}
|}


=====End Of Track=====
=====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'''.


When the two bytes following the delta-time == 0xffff, this track has no more commands.
See the [https://www.midi.org/specifications/item/gm-level-1-sound-set standard MIDI program numbers] for details.
 
{| class="wikitable"
! Offset
! Size
! Description
|-
| 0x0
| 1
| '''Program'''; AND with 0x7f for the value
|-
| 0x1
| 1
| '''Padding'''; always zero
|-
| 0x2
| colspan=2 {{unknown|End of program change}}
|}
 
=====End Of Region=====
 
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 track are non-zero, they point to a buffer of RLE-compressed
If the pitch or mod offsets in a region are non-zero, they point to a buffer of
(delta-tick, delta-value) pairs, decoding to signed 16-bit precision. The decoder must track
(delta-tick, delta-value) pairs. The delta times are encoded as unsigned 7/15 bit values
the absolute time and value, summing each consecutive update for the current time/values.
and the delta values are signed 7/15-bit values. The values are promoted to 15-bit if they
exceed their 7-bit range. In this case, the highest bit is set on the first byte in the pair
(0x80). The decoder must track the absolute time and value, summing each consecutive update
for the current time/values.
 
The byte sequence 0x80 0x00 is a special case that signals the end of the stream.


The algorithm for this RLE is different than the delta-time one for commands. It may
Similar to the command stream, when the delta time exceeds 15 bit range (>32767), extra pairs
scale down to a single byte if able.
with a zeroed value delta are inserted.


<syntaxhighlight lang="python" line="1">
<syntaxhighlight lang="python" line="1">
def DecodeRLE(in):
def SignExtend7(value):
     term = in.ReadU8()
     return (value & 0x3f) - (value & 0x40)
    total = term & 0x7f
    if term & 0x80:
        total = total * 256 + in.ReadU8()
    return total


def DecodeContinuousRLE(in):
def SignExtend15(value):
     total = 0
     return (value & 0x3fff) - (value & 0x4000)
     while True:
 
         term = DecodeRLE(in)
def DecodeSignedValue(streamIn):
         if term == 0x8000:
    byte0 = streamIn.ReadU8()
            total += 0xffff
     if byte0 & 0x80:
            dummy = in.ReadU8()
         byte1 = streamIn.ReadU8()
            continue
        return SignExtend15(byte1 | ((byte0 & 0x7f) << 8))
         total += term
    else:
         return SignExtend7(byte0)
 
def DecodeUnsignedValue(streamIn):
    byte0 = streamIn.ReadU8()
    if byte0 & 0x80:
        byte1 = streamIn.ReadU8()
        return byte1 | ((byte0 & 0x7f) << 8)
    else:
         return byte0


         if total >= 0x4000:
def DecodeDeltaPair(streamIn):
             return total - 0xffff
    ret = [0,0]
         else:
    while ret[1] == 0:
            return total
         if streamIn.PeekU16() == 0x8000:
             # The stream has ended
            break
        ret[0] += DecodeUnsignedValue(streamIn)
         ret[1] = DecodeSignedValue(streamIn)
    return ret
</syntaxhighlight>
</syntaxhighlight>


===Channel Map===
===Channel Map===


This is a simple '''u8 table''' mapping 64 SON tracks to 16 MIDI channels for instrument selection via the [[AGSC (File Format)#MIDI Setup Entry|SongGroup MIDI-Setup]].
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 SON has a non-zero tempo table offset, this song features tempo changes.  
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.


Anonymous user

Navigation menu