User Tools

Site Tools


base:a_sid_player_routine

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 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
}
base/a_sid_player_routine.txt · Last modified: 2015-04-17 04:30 (external edit)