Rare SNES SPC Sound Engine (Donkey Kong Country, etc.) Specification

This document explains about Rare's sound engine for SNES and its music format.

MIDI conversion tool (rarespc) is available at https://code.google.com/p/loveemu/ Also, I work on VGMTrans port of it.

SNES SPC700 Player and SPC700 Memory Viewer (SPCMEM) was very useful during the analysis.

Summary

The driver is used by Donkey Kong Country games and a few other games developed by Rare.

Each drivers has minor version differences, but most things are the same.

The driver design is very simple. Most events set their parameter value to S-DSP register directly.

Whole Structure

  • Note: Numbers are usually stored in little-endian, but there are some exceptions.
  • Note: Addresses ($xxxx) in this document are APU RAM address. You can convert the RAM address to an offset in SPC file, by adding 0x100 to the address.

Whole structure of sound data on APU RAM is something like the following.

[Explanatory Chart]
Region Name ($xxxx : starting address)
  + Address 1: Field Name 1 = $yyyy Example of value
  + Address 2: Field Name 2 = $zzzz Example of value

[Music Data Structure Sample]
Song Header Region
  + 12a0: Score Address for Channel 1 = $12b2
  + 12a2: Score Address for Channel 2 = $13b4
  + 12a4: Score Address for Channel 3 = $1770
  + 12a6: Score Address for Channel 4 = $17a8
  + 12a8: Score Address for Channel 5 = $1990
  + 12aa: Score Address for Channel 6 = $14d8
  + 12ac: Score Address for Channel 7 = $1587
  + 12ae: Score Address for Channel 8 = $1675
  + 12b0: Initial Tempo = $de
  + 12b1: SFX Tempo = $a0

  Score for Channel 1
    + 12b2: Events
  Score for Channel 2
    + 13b4: Events
  ...

A song has a simple header and score data for each channels.

Song Header

A song header is always located at the following address.

Each pointers to score data must be a valid address, even if you do not want to use some channels. To inactivate a channel, put an end of track event ($00) at the beginning of the score data.

The interpretation of tempo value is the same as tempo event. It is not beats per second.

SFX tempo should be the same as other songs, otherwise SFXs would become faster/slower.

Score Data

The music engine is tick-based. Timebase (TPQN) is 32.

The following is the list of score events.

  • Common Events
    • 00 - End Of Track
    • 01 xx - Set Instrument
    • 02 xx yy - Set L/R Volume
    • 03 xx yy - Jump
    • 04 xx yy zz - Call/Repeat Subroutine
    • 05 - End Subroutine
    • 06 xx (yy) - Default Duration On
    • 07 - Default Duration Off
    • 08 xx yy zz ww vv - Pitch Slide Up
    • 09 xx yy zz ww vv - Pitch Slide Down
    • 0A - Pitch Slide Off
    • 0B xx - Set Tempo
    • 0C xx - Add Tempo
    • 0D xx yy zz - Vibrato (No Delay)
    • 0E - Vibrato Off
    • 0F xx yy zz ww - Vibrato
    • 10 xx yy - Set ADSR Envelope
    • 11 xx yy - Master Volume L/R
    • 12 xx - Tuning
    • 13 xx - Transpose (Fine Tune)
    • 14 xx - Transpose (Relative)
    • 15 xx yy zz - Echo Param
    • 16 - Echo On
    • 17 - Echo Off
    • 18 xx xx xx xx xx xx xx xx - Echo Filter
    • 19 xx - Noise Frequency
    • 1A - Noise On
    • 1B - Noise Off
    • 1C xx - Set Variable Note 1
    • 1D xx - Set Variable Note 2
    • 26 xx yy zz ww - Pitch Slide Down (Simpler)
    • 27 xx yy zz ww - Pitch Slide Up (Simpler)
    • 2B - Long Duration On
    • 2C - Long Duration Off
    • 80~FF (xx (xx)) - Rest/Note
  • Donkey Kong Country
    • 1C xx yy zz ww - Set Volume L/R & ADSR Preset 1
    • 1D xx yy zz ww - Set Volume L/R & ADSR Preset 2
    • 1E xx yy zz ww - Set Volume L/R & ADSR Preset 3
    • 1F xx yy zz ww - Set Volume L/R & ADSR Preset 4
    • 20 xx yy zz ww - Set Volume L/R & ADSR Preset 5
    • 21 - Get Volume L/R & ADSR Preset 1
    • 22 - Get Volume L/R & ADSR Preset 2
    • 23 - Get Volume L/R & ADSR Preset 3
    • 24 - Get Volume L/R & ADSR Preset 4
    • 25 - Get Volume L/R & ADSR Preset 5
    • 28 xx yy zz - Set Instrument & Volume L/R
    • 29 xx - Fade Out
    • 2A xx - Set Timer 0 Frequency
    • 2D xx yy ... - Conditional Jump
    • 2E xx - Set Jump Condition
    • 2F xx yy zz ww - Tremolo
    • 30 - Tremolo Off
  • Killer Instinct
    • 1E xx - Set Volume (Center)
    • 1F xx yy - Call Subroutine Once
    • 20 - Reset ADSR
    • 21 - Reset ADSR (Soft Attack)
    • 22 xx yy zz - Set Voice Parameters (Short)
    • 23 xx - Echo Delay
    • 0C, 0D, 11, 15, 18, 19, 1A, 1B, 1C, 1D, 24, 25, 28, 29, 2A, 2D, 2E, 2F, 30 - Undefined
  • Donkey Kong Country 2, 3
    • 1E xx yy zz ww - Set Volume Preset 1 & 2
    • 1F xx - Echo Delay
    • 20 - Volume From Preset 1
    • 21 xx yy - Call Subroutine Once
    • 22 xx yy zz ww vv uu tt - Set Voice Parameters
    • 23 xx - Set Volume (Center)
    • 24 xx - Master Volume
    • 31 - Volume From Preset 2
    • 11, 25, 28, 29, 2A, 2D, 2E, 2F - Unidentified
    • 30, 32 - Same as Event 17, Echo Off (should not be used)
  • Ken Griffey Jr. Winning Run
    • 20 xx - Master Volume
    • 21 xx - Set Volume (Center)
    • 22 xx yy zz - NOP
    • 23 xx yy - Call Subroutine Once
    • 24 - Pitch Slide/Vibrato/Tremolo Off
    • 25 xx yy zz ww - ?
    • 28 xx yy zz - Set Instrument & Volume L/R
    • 29 xx - Fade Out
    • 2A xx - Set Timer 0 Frequency
    • 2F xx yy zz ww - Tremolo
    • 30 - Tremolo Off
    • 19, 1A, 1B, 1E, 1F, 2D, 2E, 31 - Unidentified
80~FF (xx (xx)), E0, E1 [Rest/Note]

The note byte itself specifies the key of note (use $80 for rest). The note number of the engine is higher by 36 (i.e. 3 octaves) than MIDI note number.

The next parameter is the length of note, however:

  • If the default duration is on, it does not take length bytes. (see also: event $06, $07)
  • If the long duration is on, it takes length in 2 bytes (big-endian). Otherwise the length is 1 byte. (see also: event $2b, $2c)

There is no "tie" function because the engine can use a long note without it.

There is a little more additional rules except Donkey Kong Country.

  • If note byte is $e0, use the value set by event $1c as actual note byte.
  • If note byte is $e1, use the value set by event $1d as actual note byte.
  • $e2-ff should work as the same as $e0. They should not be used though.

S-DSP pitch register value is determined by the following table. You might want to know it if you would try to convert pitch slide events into MIDI pitch events.

const uint16_t NOTE_PITCH_TABLE[] = {
    0x0000, 0x0040, 0x0044, 0x0048, 0x004c, 0x0051, 0x0055, 0x005b,
    0x0060, 0x0066, 0x006c, 0x0072, 0x0079, 0x0080, 0x0088, 0x0090,
    0x0098, 0x00a1, 0x00ab, 0x00b5, 0x00c0, 0x00cb, 0x00d7, 0x00e4,
    0x00f2, 0x0100, 0x010f, 0x011f, 0x0130, 0x0143, 0x0156, 0x016a,
    0x0180, 0x0196, 0x01af, 0x01c8, 0x01e3, 0x0200, 0x021e, 0x023f,
    0x0261, 0x0285, 0x02ab, 0x02d4, 0x02ff, 0x032d, 0x035d, 0x0390,
    0x03c7, 0x0400, 0x043d, 0x047d, 0x04c2, 0x050a, 0x0557, 0x05a8,
    0x05fe, 0x065a, 0x06ba, 0x0721, 0x078d, 0x0800, 0x087a, 0x08fb,
    0x0984, 0x0a14, 0x0aae, 0x0b50, 0x0bfd, 0x0cb3, 0x0d74, 0x0e41,
    0x0f1a, 0x1000, 0x10f4, 0x11f6, 0x1307, 0x1429, 0x155c, 0x16a1,
    0x17f9, 0x1966, 0x1ae9, 0x1c82, 0x1e34, 0x2000, 0x21e7, 0x23eb,
    0x260e, 0x2851, 0x2ab7, 0x2d41, 0x2ff2, 0x32cc, 0x35d1, 0x3904,
    0x3c68, 0x3fff
};
00 [End Of Track]

Stop the activity of the channel (halt).

01 xx [Set Instrument]

Set instrument. Actual sample number is obtained from xxth element of SRCN table.

SRCN table is a 256 bytes array. It is always located at the following address.

02 xx yy [Set L/R Volume]

Set channel volume. xx is the left volume, and yy is the right volume. Their values will be set directly to S-DSP registers, VOL(L) and VOL(R).

If the value is signed, the channel output becomes reverse-phase.

You might want to convert the S-DSP volume values into MIDI volume and panpot. The following function calculates the linear volume and panpot first, then converts them into common GM2 MIDI scale.

function CalcVolPanFromDSPVol(dspVolL, dspVolR)
    local round = function(x)
        return math.floor(x + 0.5)
    end

    local volL = math.abs(dspVolL) / 128.0
    local volR = math.abs(dspVolR) / 128.0

    -- linear volume and panpot
    local linearVol = (volL + volR) / 2
    local linearPan = volR / (volL + volR)

    print("Volume (Linear)", linearVol)
    if linearVol ~= 0 then
        print("Panpot (Linear)", linearPan)
    end

    -- GM2 compatible
    local midiVol = 0
    local midiPan = 64
    if linearVol ~= 0 then
        local panPI2 = math.atan2(linearPan, 1.0 - linearPan)
        midiVol = round(math.sqrt(linearVol / (math.cos(panPI2) + math.sin(panPI2))) * 127)
        midiPan = round((panPI2 / (math.pi / 2)) * 126) + 1
    end

    print("Volume (GM2)", midiVol)
    if linearVol ~= 0 then
        print("Panpot (GM2)", midiPan)
    end
end
03 xx yy [Jump]

Jump to $yyxx (goto). This event can be used for infinite loop.

04 xx yy zz [Call/Repeat Subroutine]

Play $zzyy in xx times. Subroutine must end with event $05.

This event will push 2 bytes to the return address stack. Since the stack size is 8 bytes, maximum nest level is 4.

05 [End Subroutine]

Jump to the return address saved by event $04.

06 xx (yy) [Default Duration On]

Set the default duration switch on, and set the default length.

If the long duration is on, it takes length in 2 bytes (big-endian). Otherwise the length is 1 byte. (see also: event $2b, $2c)

07 [Default Duration Off]

Set the default duration switch off.

08 xx yy zz ww vv [Pitch Slide Up]

Turn pitch slider on, to make the pitch change at key-on.

This event will fall the pitch first, then rise the pitch after that immediately. (just imagine a V-curve)

xx
Pitch Slide Delay (timer 0 clocks)
yy
Pitch Slide Interval (timer 0 clocks)
zz
Number Of Times Of Pitch Up/Down
ww
Pitch Delta (frequency, signed)
vv
Number Of Times Of Pitch Down (i.e. opposite direction)

Timings are independent of song tempo, so that you can use fast envelopes even in a slow song. Timings can be converted to milliseconds by the following formula.

milliseconds = (0.125 * DSP.regs[0xfa] * value); // dsp $fa = timer 0 frequency

Donkey Kong Country and Winning Run can change timer 0 frequency by an event, others (DKC2 and Killer Instinct) always use constant frequency ($fa = 100).

Pitch amount is specified by a delta of S-DSP pitch register value. You can convert the delta value to cent by the following function. Note that you will need the current frequency of note as a parameter.

function PitchDeltaToCent(currentPitch, delta)
    local scale = (currentPitch + delta) / currentPitch
    local cent = 1200 * math.log(scale) / math.log(2)
    print(cent)
end

If you are a composer and you want to calculate the delta value by current frequency and cent value, you can throw the following query to Google.

currentPitch * (2^(cent/1200) - 1.0)

example: 0x06ba * (2^(100/1200) - 1.0)
09 xx yy zz ww vv [Pitch Slide Down]

It is the same as Pitch Slide Up, except the pitch change direction.

xx
Pitch Slide Delay (timer 0 clocks)
yy
Pitch Slide Interval (timer 0 clocks)
zz
Number Of Times Of Pitch Up/Down
ww
Pitch Delta (frequency, signed)
vv
Number Of Times Of Pitch Up (i.e. opposite direction)
0A [Pitch Slide Off]

Turn pitch slider off.

0B xx [Set Tempo]

Set tempo. Song tempo can be converted into BPM (beats per minute) by the following formula.

const int TPQN = 32;
assert(DSP.regs[0xfa] != 0);
assert(tempo != 0);
bpm = 60000000.0 / (TPQN * (125 * DSP.regs[0xfa])) * (tempo / 256.0); // dsp $fa = timer 0 frequency
0C xx [Add Tempo]

Add xx to the current tempo, where xx is a signed value.

0D xx yy zz [Vibrato (No Delay)]

It is the same as Vibrato with delay=0. (0F xx yy zz 00)

0E [Vibrato Off]

Turn vibrato off.

0F xx yy zz ww [Vibrato]

Turn vibrato (pitch LFO) on. The output frequency is vibrated by a triangle wave.

xx
Vibrato Length (steps per cycle, must be >= 2)
yy
Vibrato Rate (Interval) (ticks per step)
zz
Vibrato Depth (pitch frequency delta per step)
ww
Vibrato Delay (ticks)
10 xx yy [Set ADSR Envelope]

Set ADSR values. xx is the ADSR(1) value, and yy is the ADSR(2) value.

The following figure is quoted from APU manual by Ledi, for who are not familiar with S-DSP register values.

   (3) ADSR(1), ADSR(2)

                  D7    D6  D5  D4  D3  D2  D1  D0
                ----------------------------------
      ADSR(1)   |ADSR |    DR     |     AR        |
      (05H)     |/GAIN|           |               |
                |-----|---|---|---|---|---|---|---|
      ADRS(2)   |       SL               SR       |
      (06H)     |                                 |
                |-----|---|---|---|---|---|---|----

    When D7 of ADSR(1) =1, these two bytes become operable (ADSR mode)
11 xx yy [Master Volume L/R]

Set master volume. xx is the left volume, and yy is the right volume. Their values will be set directly to S-DSP registers, MVOL(L) and MVOL(R).

If the value is signed, the channel output becomes reverse-phase.

Note that this event changes the MVOL register directly. It will also change the SFX volumes.

12 xx [Tuning]

Adjust the pitch of note. Amount is a signed value, and it is processed by the following code.

int base_pitch = pitch_table[note_byte + 36 + transpose];
int tuning = base_pitch * abs(xx) / 1024;
int pitch = base_pitch + (xx >= 0 ? tuning : -tuning);

The code above can be rewritten like the following.

int pitch = (base_pitch * (1024 + xx) + (xx < 0 ? 1023 : 0)) / 1024;

The formula for the amount in cents is, cents = 1200 * log((1024 + xx) / 1024) / log(2)

Note that the tuning changes pitch slide amount, too (because both are based on frequency).

13 xx [Transpose (Fine Tune)]

Set the transpose amount to xx semitones, where xx is a signed value.

This event should be used only for correcting the pitch difference between score data and BRR samples. MIDI converter should ignore this event, or the output will become strange.

14 xx [Transpose (Relative)]

Increase (Decrease) the transpose amount to by xx semitones, where xx is a signed value.

This event should be used when you want to play existing phrases in different scale. (Bad Bird Rag of DKC2 might be a good example for this)

15 xx yy zz [Echo Param]

Set echo feedback and volume.

xx
Echo Feedback
yy
Echo Volume (L)
zz
Echo Volume (R)
16 [Echo On]

Enable channel echo.

17 [Echo Off]

Disable channel echo.

18 xx xx xx xx xx xx xx xx [Echo Filter]

Set echo FIR filter.

Many of SNES music engines use the following filters, for your information.

0: 7F 00 00 00 00 00 00 00
1: 58 BF DB F0 FE 07 0C 0C
2: 0C 21 2B 2B 13 FE F3 F9
3: 34 33 00 D9 E5 01 FC EB
19 xx [Noise Frequency]

Set noise frequency. xx=$00~1f (NCK register value)

1A [Noise On]

Enable noise sound.

1B [Noise Off]

Disable noise sound.

1C xx [Set Variable Note 1]

Set the note byte for event $e0. See the note event for details.

This event will allow you to use the same phrase by different key. Krook's March (DKC2) is one of examples of use.

1D xx [Set Variable Note 2]

Set the note byte for event $e1. See the note event for details.

26 xx yy zz ww [Pitch Slide Down (Simpler)]

It is the same as 09 xx yy zz*2 ww zz.

xx
Pitch Slide Delay (timer 0 clocks)
yy
Pitch Slide Interval (timer 0 clocks)
zz
Number Of Times Of Pitch Change (see full size event for details)
ww
Pitch Delta (frequency, signed)
27 xx yy zz ww [Pitch Slide Up (Simpler)]

It is the same as 08 xx yy zz*2 ww zz.

xx
Pitch Slide Delay (timer 0 clocks)
yy
Pitch Slide Interval (timer 0 clocks)
zz
Number Of Times Of Pitch Change (see full size event for details)
ww
Pitch Delta (frequency, signed)
2B [Long Duration On]

Set the long duration switch on. It affects to the length of duration bytes.

2C [Long Duration Off]

Set the long duration switch off. It affects to the length of duration bytes.

(DKC:1C~20) xx yy zz ww [Set Volume L/R & ADSR Preset 1~5]

Set the frequently used volume/ADSR pair. The registered values will be used by event $21~25.

xx
VOL(L)
yy
VOL(R)
zz
ADSR(1)
ww
ADSR(2)
(DKC:21~25) [Get Volume L/R & ADSR Preset 1~5]

Read volume/ADSR from preset. Values can be set by event $1c~20.

(DKC:28, WR:28) xx yy zz [Set Instrument & Volume L/R]

Set new instrument and volume values at the same time.

xx
Instrument
yy
VOL(L)
zz
VOL(R)
(DKC:29, WR:29) xx [Fade Out]
xx
Fade Out Length (ticks)

Start fading out the S-DSP Master Volume register values.

(DKC:2A, WR:2A) xx [Set Timer 0 Frequency]

Set timer 0 frequency (i.e. DSP register $fa)

Frequency (Hz) = 1.0 / (0.000125 * value)

It affects to music tempo and pitch slider speed.

(DKC:2D) xx yy ... [Conditional Jump]

Jump to N-th address of the parameters, where N is set by event $2e.

(DKC:2E) xx [Set Jump Condition]

Set the jump destination index of event $2d.

(DKC:2F, WR:2F) xx yy zz ww [Tremolo]

Turn tremolo (volume LFO) on. The output volume is vibrated by a triangle wave.

xx
Tremolo Length (steps per cycle, must be >= 2)
yy
Tremolo Rate (Interval) (ticks per step)
zz
Tremolo Depth (pitch frequency delta per step)
ww
Tremolo Delay (ticks)
(DKC:30, WR:30) [Tremolo Off]

Turn tremolo off.

(KI:1E, DKC2:23, WR:21) xx [Set Volume (Center)]

Set the left and right volume to xx.

(KI:1F, DKC2:21, WR:23) xx yy [Call Subroutine Once]

It is the same as 04 01 xx yy. Play $yyxx once.

(KI:20) [Reset ADSR]

It is the same as 10 8F E0. (ADSR1,2=$8f,$e0)

(KI:21) [Reset ADSR (Soft Attack)]

It is the same as 10 8E E0. (ADSR1,2=$8e,$e0)

(KI:22) xx yy zz [Set Voice Parameters (Short)]

Set multiple voice parameters at the same time.

xx
Instrument
yy
Transpose
zz
Tuning
(DKC2:22) xx yy zz ww vv uu tt [Set Voice Parameters]

Set multiple voice parameters at the same time.

xx
Instrument
yy
Transpose
zz
Tuning
ww
VOL(L)
vv
VOL(R)
uu
ADSR(1)
tt
ADSR(2)
(KI:23, DKC2:1F) xx [Echo Delay]

Set echo delay.

(DKC2:1E) xx yy zz ww [Set Volume Preset 1 & 2]

Set the frequently used volume values. The registered values will be used by event $20 and $31.

xx
VOL(L) of Preset 1
yy
VOL(R) of Preset 1
zz
VOL(L) of Preset 2
ww
VOL(R) of Preset 2
(DKC2:20) [Volume From Preset 1]

Read volume/ADSR from preset 1. Values can be set by event $1e.

(DKC2:31) [Volume From Preset 2]

Read volume/ADSR from preset 2. Values can be set by event $1e.

(DKC2:24, WR:20) xx [Master Volume]

Set master volume. xx is unsigned 8 bit value.

This event does not change S-DSP MVOL register. The value is just a coefficient for channel volume.

(WR:22) xx yy zz [NOP (No Operations)]

Do nothing.

(WR:24) [Pitch Slide/Vibrato/Tremolo Off]

Disable pitch slide, vibrato, and tremolo at the same time.

(WR:25) xx yy zz ww [?]

Unknown event.

MIDI Conversion Note

  • All tracks must be parsed at the same time, not one by one, or the conversion will fail at some points.
    • In Bad Bird Rag (DKC2), a track increases tempo, and other track resets tempo.
    • In Stickerbrush Symphony (DKC2), tracks have different loop points. First track (marimba) repeats a short phrase infinitely. The global loop point of the song cannot be known without parsing other tracks.
  • Backjump is sometimes not an infinite loop point. In Stickerbrush Symphony (DKC2), a backjump during a subroutine is located in the marimba track.
  • The location of score data for each tracks is not sorted by a channel number. Additionally, a channel score can cross the other one sometimes.