CSNG (File Format): Difference between revisions

Jump to navigation Jump to search
>MrSinistar
No edit summary
imported>Jackoalan
 
(19 intermediate revisions by 2 users 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.  


The offsets are absolute and must be located with the CSNG header removed.
__TOC__
 
== Format ==


{{research|3|Nothing is known about this format.}}
All offsets are relative to the start of the main header (after the custom header).  


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


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


=== Custom Header ===
=== Custom Header ===
Line 24: Line 30:
| 0x4
| 0x4
| 4
| 4
| '''Sequence Index'''
| '''MIDI Setup ID'''
|-
|-
| 0x8
| 0x8
| 4
| 4
| '''Voice Count'''
| '''SongGroup ID'''
|-
|-
| 0xC
| 0xC
Line 36: Line 42:
| 0x10
| 0x10
| 4
| 4
| '''File Length'''
| '''SNG File Length'''
|-
|-
| 0x14
| 0x14
Line 51: Line 57:
| 0x0
| 0x0
| 4
| 4
| '''Voice Header Table Offset''' (start of MusyX SON data)
| '''Track Index Offset'''; usually 0x18 for GCN games
|-
|-
| 0x4
| 0x4
| 4
| 4
| '''Voice Sequence Offsets Table Offset'''
| '''Region Data Index Offset'''
|-
|-
| 0x8
| 0x8
| 4
| 4
| '''Wave Index Table Offset'''
| '''Channel Map Offset'''
|-
|-
| 0xC
| 0xC
| 4
| 4
| {{unknown|'''Unknown'''}}; (always 0...probably another offset)
| '''Tempo Table Offset'''; 0x0 if tempo doesn't change
|-
|-
| 0x10
| 0x10
| 4
| 4
| '''Initial BPM Rate'''; AKA Tempo (always 0x78 = 120 beats per minute)
| '''Initial Tempo'''; (commonly 0x78 or 120 beats per minute)
|-
| 0x14
| 4
| {{unknown|'''Unknown'''}}
|-
| 0x18
| 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"
! Offset
! Size
! Description
|-
| 0x0
| 4
| '''Start Tick'''; time-point to begin executing region data
|-
| 0x4
| 4
| {{unknown|'''Unknown'''}}; commonly 0xffff0000
|-
| 0x8
| 2
| '''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
| 2
| '''Loop Region Index'''; index of region to jump to if ''Region Data Index'' set to -2
|-
| 0xC
| colspan=2 {{unknown|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====
 
{| class="wikitable"
! 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
| colspan=2 {{unknown|End of region data}}
|}
 
====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 Encoding=====
 
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 fixed 16-bit
value to store the delta times. In case of a delta time greater than 65535, no-op commands
with a 65535 delta time are inserted into the command stream until the target time is attained.
 
In a theoretical Python streaming API, the delta time can be decoded with no-ops automatically consumed like so:
 
<syntaxhighlight lang="python" line="1">
def DecodeDeltaTime(streamIn):
    total = 0
    while True:
        term = streamIn.ReadU16()
        next = streamIn.PeekU16()
        if next == 0:
            # Automatically consume no-op and continue accumulating time value
            total += 0xffff
            streamIn.seekAhead(2)
            continue
        total += term
        return total
</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=====
 
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.
 
{| class="wikitable"
! 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
|-
|-
| colspan=3 {{unknown|WIP}}
| 0x4
| colspan=2 {{unknown|End of note}}
|}
|}


==Data==
=====Control Change Command=====


The data in the files is very similar to MIDI, however it is formatted by word, rather than MIDI's byte formatting.
When the two bytes following the delta-time != 0xffff, and the high-bits of both bytes are ''set'',
this is a '''control change command'''.


After the data for the instruments are defined, there is a table of offsets for data through out the SON data.  Each chunk of data starts with a long 0x8, usually followed by another absolute offset that points to the end of the chunk.
See the [https://www.midi.org/specifications/item/table-3-control-change-messages-data-bytes-2 standard MIDI control numbers] for details.


Notes durations are controlled by 96 ticks per beat.
{| class="wikitable"
! Offset
! Size
! Description
|-
| 0x0
| 1
| '''Value'''; AND with 0x7f for the value
|-
| 0x1
| 1
| '''Control'''; AND with 0x7f for the value
|-
| 0x2
| colspan=2 {{unknown|End of control change}}
|}
 
=====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 [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====
 
If the pitch or mod offsets in a region are non-zero, they point to a buffer of
(delta-tick, delta-value) pairs. The delta times are encoded as unsigned 7/15 bit 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.
 
Similar to the command stream, when the delta time exceeds 15 bit range (>32767), extra pairs
with a zeroed value delta are inserted.
 
<syntaxhighlight lang="python" line="1">
def SignExtend7(value):
    return (value & 0x3f) - (value & 0x40)
 
def SignExtend15(value):
    return (value & 0x3fff) - (value & 0x4000)
 
def DecodeSignedValue(streamIn):
    byte0 = streamIn.ReadU8()
    if byte0 & 0x80:
        byte1 = streamIn.ReadU8()
        return SignExtend15(byte1 | ((byte0 & 0x7f) << 8))
    else:
        return SignExtend7(byte0)
 
def DecodeUnsignedValue(streamIn):
    byte0 = streamIn.ReadU8()
    if byte0 & 0x80:
        byte1 = streamIn.ReadU8()
        return byte1 | ((byte0 & 0x7f) << 8)
    else:
        return byte0
 
def DecodeDeltaPair(streamIn):
    ret = [0,0]
    while ret[1] == 0:
        if streamIn.PeekU16() == 0x8000:
            # The stream has ended
            break
        ret[0] += DecodeUnsignedValue(streamIn)
        ret[1] = DecodeSignedValue(streamIn)
    return ret
</syntaxhighlight>
 
===Channel Map===
 
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===
 
When the SNG has a non-zero tempo table offset, this song features tempo changes.
The change events are simple absolute-tick / BPM pairs.
 
{| class="wikitable"
! Offset
! Size
! Description
|-
| 0x0
| 4
| '''Tick'''; absolute time-point to perform tempo change
|-
| 0x4
| 4
| '''Tempo'''; new tempo in BPM
|-
| 0x2
| colspan=2 {{unknown|End of tempo change}}
|}


[[Category:File Formats]]
[[Category:File Formats]]
[[Category:Metroid Prime]]
[[Category:Metroid Prime]]
[[Category:Metroid Prime 2: Echoes]]
[[Category:Metroid Prime 2: Echoes]]
Anonymous user

Navigation menu