====== A SID player routine ======
By Linus Wallej (King Fisher/Triad)
This document should really be an extensive "how do SIDplayers work?" in order to be perfect, but you cannot have everything. The following pseudo source code does not include the ability to play 4-bit samples (which is obviously easy to achieve too).
I learned how SIDplayers worked when I wrote the MIDI program for C64 called [[https://www.df.lth.se/~triad/triad/ftp/MIDI/|MIDIslave]]. I also learned a lot from the editors I used on C64, which includes: soundmonitor (by Chris Hülsbeck), Future Composer (by Finnish Gold), Rockmonitor, Pro-Drum and Music Assembler (all by Dutch USA-team), The JCH Editor (by JCH), Soundtracker 64 (by Mechanix), Soedesoft Editor (by Jeroen Soede) and many more. I wrote this document on request from Michael Kleps, the man that put M.K. into the extended Soundtracker module files and also the author of QuadraSID, a Cubase VST instrument.
A note on "hard restart": The JCH player on C64 is a very good one. What makes this and some other c64 players better than others is that it utilizes something called "hard restart". That is: the SID provides quite bad accuracy on attack after releasing a note, and one way to solve this was hard restart, which is to write 0x00 to all registers at $d400--$d406 2/50 second (2 frames) before next attack. (The actual minimum time is 33 ms = 2^15 cycles according to Dag Lem, author of reSID.) Some say setting the test bit by putting 0x08 into the voice control register $d412 (etc) is just as good. This problem only affects the 6581 version of the SID chip (or so I am told...) and not the later 8580 version, so for some MIDI appliances the 8580 is a lot better choice. (Even though it doesn't have the same groovy filter as the 6581 which is a totally different story.) Hard restart only fixes problems with attack, not the ever-present problem with release.
In order to play MIDI sounds "hard restart" requires you to be able to "see into the future" and know 1/50 second before the next attack, that it is actually going to happen. On MIDI players, this is a catch 22: you don't just know what is gonna happen in 1/50 second from now. The SIDstation has two ways of solving this: either you "hard restart" every voice on keypress, which delays all notes 1/50 second. Or you "hard restart" on note off (key release), which kills the voice effectively, and makes all release settings superfluous.
===== The Pseudo-Code =====
This is a simple demonstration of algorithms and priciples for a MIDI controlled SID player.
// Some (not all!) arrays that will be needed
int note_is_on [3]
int has_macro [3]
int macro_speed [3]
int initial_vibrato_delay [3]
int initial_pw_vibrato_delay [3]
int has_vibrato [3]
// To be called at instrument change
instrument_on() {
// Set up global registers for this instrument
if (instrument_has_filter) {
instrument_default_lo_filter -> $d415
instrument_default_hi_filter -> $d416
instrument_default_resonance & 0xF0 | 0x0F -> $d417
}
else
0x00 -> $d417
get_instrument_filter_type() | 0x0F -> $d418
}
// To be called at keypress
note_on() {
// The allocation assumes 3 channels only. Of course you could
// add exotic things like new SID emulation instances being added
// at will, or say two or three SIDs by default.
if (empty_channel_not_available &&
user_wants_new_notes_to_punch_out_old_ones) {
// The note_is_on[] array is good to use for this
Deallocate_a_channel_offset_using_FIFO_principle()
}
if (channel = Allocate_and_fetch_channel_ok()) {
note_is_on[channel] = true
offset = fetch_channel_offset(channel)
fetch_default_values_for_current_instrument
lo_frequency_from_MIDI -> $d400 + offset
hi_frequency_from_MIDI -> $d401 + offset
if (default_waveform_is_pulse (== 0x40)) {
lo_default_pulsewidth -> $d402 + offset
hi_default_pulsewidth -> $d403 + offset
}
// Note that this byte also controls ring modulation
// and synchronization
default_waveform | 0x01 -> $d404 + offset
default_attack_and_decay -> $d405 + offset
default_sustain_and_release -> $d406 + offset
// add_macro_for_channel(offset)
if (this_instrument_has_a_waveform_macro) {
has_macro[channel] = true
// This value is relative to process frequency
// Default speed could be 8, see below
macro_speed[channel] = this_instrument_default_speed
}
if (this_instrument_has_vibrato) {
// This value is relative to process frequency
initial_vibrato_delay[channel] =
this_instrument_delay_before_vibrato
}
if (this_instrument_has_pulse_vibrato) {
// This value is relative to process frequency
initial_pw_vibrato_delay[channel] =
this_instrument_delay_before_pw_vibrato
}
}
}
// This routine to be called a reasonably high rate, say
// 400 Hz or so. The macro speed on a C64 is usually 50 Hz
// so this should be the default "delta" factor. The max
// macro speed is thus 8 times in a vframe (50 Hz) the
// highest rate I have seen in a C64 playroutine is 600 Hz.
note_process() {
for (channel=0; channel < 3; channel++) {
offset = fetch_channel_offset(channel)
if (has_macro[channel]) {
// delta--, default delta = 8 for 400 Hz
if (!macro_speed[channel]--) {
// Reset the divisor
macro_speed[channel] = this_instrument_default_speed
// Update waveform macro, the routine
// get_next_wavebyte() should retrieve a
// byte from a user-editable table, which
// can either:
// 1) Loop from a certain point, or
// 2) End with a certain waveformbyte
// typically these macros ARE allowed to
// alter also the ringmod and sync bits.
get_next_wavebyte() & 0xFE |
note_is_on[channel] ? 0x01 : 0x00 -> $d404 + offset
// Update arpeggio macro, only interesting
// if you don't use vibrato on this voice
// really.
if(!instrument_has_vibrato) {
// This table can loop or end.
// arpeggios are usually bytes which represents
// the number of halftones to transpose the current
// note UPWARDS. For example macro 0x00 0x03 0x07
// creates a minor chord.
arp = get_next_arpeggio_byte()
offset_from_base_frequency_lo(arp) -> $d400 + offset
offset_from_base_frequency_hi(arp) -> $d401 + offset
}
get_next_arpeggio_value()
// The pulsewidth is typically included in
// the macro, if no pw_vibrato is chosen
if (!instrument_has_pw_vibrato) {
// This table can also loop or end,
// of course you could add a byte
// for $d402 also, but most c64
// players don't do this.
get_next_pw_byte() -> $d403 + offset
}
// If the filter is not controlled by
// wheel, then use a macro table with same
// functions for this too. Also here, it
// is possible to use 16bit resolution,
// but most c64 players use only the
// high byte. The resonance byte is also
// included in most players, even though
// it has dangerous effects like switching
// filter off or on for current channel.
// Some will mask off the low nybble for
// this, which is my choice.
if (instrument_has_filter_on && !get_filter_cutoff_from_wheel) {
// This table can also loop or
// end.
get_next_filter_hi_byte() -> $d416 + offset
get_next_resonance_byte() & 0xF0 | 0x0F -> $d417 + offset
}
}
}
if (this_instrument_has_vibrato && !inital_vibrato_delay[channel]--) {
initial_vibrato_delay[channel] = 1
// Update vibrato
// This is done by adding this instruments
// default addititve curve over the
// values in $d400/$d401. An amplitude
// default for this instrument should
// also exist if vibrato is used.
// In case you are not using wheels, a
// LFO (Low Frequency Oscillator) can be used to
// add a certain amplitude and period over the pulsewidth.
if (get_vibrato_amplitude_from_wheel)
amplitude = get_wheel_value()
else
amplitude = this_instrument_default_vibrato_amplitude
// The period will be relatie to the
// processing frequency period = lambda
if (get_vibrato_frequency_from_wheel)
period = get_wheel_value()
else
period = this_instrument_default_frequency
// update $d400/$d401 in this routine
// Make sure this function takes into account
// the current pitchweel value, if it is to be
// used!
vibrate_channel(channel, amplitude, period)
}
if (this_instrument_has_pw_vibrato && !initial_pw_vibrato_delay[channel]--) {
initial_pw_vibrato_delay[channel] = 1
// Update pulsewidth vibrato
// Similar to usual vibrato, but larger amplitude
// can be used. In case you are not using wheels, a
// LFO (Low Frequency Oscillator) can be used to
// add a certain amplitude and period over the pulsewidth.
if (get_pw_vibrato_from_wheel)
amplitude = get_wheel_value()
else
amplitude = this_instrument_default_pw_vibrato_amplitude
if (get_pw_vibrato_frequency_from_wheel)
period = get_wheel_value()
else
period = this_instrument_default_pw_vibrato_period
// update $d402/$d403 in this routine
pw_vibrate_channel(channel, amplitude, period)
}
if (instrument_has_filter && get_filter_from_wheel) {
// Modulate filter with algorithms using sinus
// or sawtooth, or square wave, or read the hard
// value from the wheel for a TB303-like-effect.
get_next_filter_lo_value() -> $d415
get_next_filter_hi_value() -> $d416
// You can of course have separate modulation
// or wheel for the resonance. Remeber that it is
// just 4 bits though!
get_next_filter_resonance() | 0xF0 | 0x0F -> $d417
}
}
}
note_off() {
// Nothing else should be done. See note above on "hard restart"
note_is_on[channel] = false
offset = get_offset_from_channel(channel)
$d412 + offset & 0xFE -> $d412 + offset
}
===== Note tables =====
Unless you calculate the note values by maths (which is preferable, especially to modulate the pulsewidth and such), the following table may be useful for getting some sounds out of the 6581:
/*
* Notetable: these values represents notes on a C64
* SID chip. Pick a value from each vector for correct
* frequency parameters, note_hi[x] = $d400, note_lo[x] = $d401
* The numbers in the C64 hardware reference manual are simply
* WRONG. Index 0 = C-0, index 36 = C-3 (flat C),
* index 57 = A-4 (flat A), index 95 = A-7 (last B in octave 8
* is not possible to replay with c64)
*
* Public Domain - Linus Walleij 2001
*/
unsigned char note_hi[95] = {
0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,0x01
0x02,0x02,0x02,0x02,0x02,0x02
0x02,0x03,0x03,0x03,0x03,0x03
0x04,0x04,0x04,0x04,0x05,0x05
0x05,0x06,0x06,0x06,0x07,0x07
0x08,0x08,0x09,0x09,0x0a,0x0a
0x0b,0x0c,0x0d,0x0d,0x0e,0x0f
0x10,0x11,0x12,0x13,0x14,0x15
0x17,0x18,0x1a,0x1b,0x1d,0x1f
0x20,0x22,0x24,0x27,0x29,0x2b
0x2e,0x31,0x34,0x37,0x3a,0x3e
0x41,0x45,0x49,0x4e,0x52,0x57
0x5c,0x62,0x68,0x6e,0x75,0x7c
0x83,0x8b,0x93,0x9c,0xa5,0xaf
0xb9,0xc4,0xd0,0xdd,0xea,0xf8
}
unsigned char note_lo[95] = {
0x16,0x27,0x38,0x4b,0x5e
0x73,0x89,0xa1,0xba,0xd4,0xf0
0x0d,0x2c,0x4e,0x71,0x96,0xbd
0xe7,0x13,0x42,0x74,0xa8,0xe0
0x1b,0x59,0x9c,0xe2,0x2c,0x7b
0xce,0x27,0x84,0xe8,0x51,0xc0
0x36,0xb3,0x38,0xc4,0x59,0xf6
0x9d,0x4e,0x09,0xd0,0xa2,0x81
0x6d,0x67,0x70,0x88,0xb2,0xed
0x3a,0x9c,0x13,0xa0,0x44,0x02
0xda,0xce,0xe0,0x11,0x64,0xda
0x75,0x38,0x26,0x40,0x89,0x04
0xb4,0x9c,0xc0,0x22,0xc8,0xb4
0xeb,0x71,0x4c,0x80,0x12,0x08
0x68,0x38,0x80,0x45,0x90,0x68
0xd6,0xe3,0x98,0x00,0x24,0x10
}