Skip to content →

SID – synthesizer and chiptunes

A big part of the C64’s charme is coming from its sound chip, the SID – Sound Interface Device. It is able to play 3 separate voices, each of them has multiple waveforms, filters and extra features, that we will cover on this page.

Timing & Implementation

To be able to get a proper timing, we need to know a few numbers and figures.

CLOCK_PAL  =  985248 Hz
CLOCK_NTSC = 1022727 Hz

Sample Rate: 44100 Hz

Use sample every ~22 ticks (PAL)
Use sample every ~23 ticks (NTSC) 
#define SAMPLE_STEPS 22
#define CLOCK_RATE 985250

This will bring us in the ballpark of being accurate with the time. We can still modify the sample rate and the sample steps later on.

void SID_init() {
	SDL_setenv("SDL_AUDIODRIVER", "directsound", 1);

	// Open our audio device; Sample Rate will dictate the pace of our synthesizer
	SDLInitAudio(44100, 1024);

	if (!SoundIsPlaying)
	{
		SDL_PauseAudio(0);
		SoundIsPlaying = true;
	}

	//	init channels
	channels.push_back(Channel());
	channels.push_back(Channel());
	channels.push_back(Channel());

	//	set channels to sync to, when SYNC bit is set 
        //      and to be synced from when RING bit is set
	channels[2].setSyncToChannel(&channels[0]);
	channels[0].setSyncToChannel(&channels[1]);
	channels[1].setSyncToChannel(&channels[2]);
	channels[0].setSyncFromChannel(&channels[2]);
	channels[1].setSyncFromChannel(&channels[0]);
	channels[2].setSyncFromChannel(&channels[1]);
}

So with our initial SDL setup and the channels set up as well, we can now have a proper function to tick through the channels.

void SID_step() {
	if (++res_count >= SAMPLE_STEPS) {
		res_count = 0;

		//	tick channels
		channels[0].tick(volume);
		channels[1].tick(volume);
		channels[2].tick(volume);

		//	play last 100 samples of all channels
		if (channels[0].buffer.size() >= 100 && channels[1].buffer.size() >= 100 && channels[2].buffer.size() >= 100) {

			for (int i = 0; i < 100; i++) {
				float res = 0;
				if (PLAY_CHANNEL1)
					res += channels[0].buffer.at(i);
				if (PLAY_CHANNEL2)
					res += channels[1].buffer.at(i);
				if (PLAY_CHANNEL3)
					res += channels[2].buffer.at(i);
				Mixbuf.push_back(res / (PLAY_CHANNEL1 + PLAY_CHANNEL2 + PLAY_CHANNEL3));
			}
			//	send audio data to device;
			SDL_QueueAudio(1, Mixbuf.data(), Mixbuf.size() * sizeof(float));

			channels[0].buffer.clear();
			channels[1].buffer.clear();
			channels[2].buffer.clear();
			Mixbuf.clear();

			while (SDL_GetQueuedAudioSize(1) > 4096 * 8) {}
		}
	}
}

Waveforms

Triangle

Triangle waveform

Sawtooth

Sawtooth waveform

Pulse

Pulse waveform

Noise

Noise waveform

Voices and addressing

The SID offers 3 Voices, each of which can make use of any of the above waveforms. To use those (and more) we need to be able to set a few settings, and therefore have the following addressing for SID and its components.

ADDR     Bit7  Bit6  Bit5  Bit4  Bit3  Bit2  Bit1  Bit0  Descr.   R/W
-----------------------------------------------------------------------

Voice 1: 

D400     F7    F6    F5    F4    F3    F2    F1    F0    FREQ LO  Write 
D401     F15   F14   F13   F12   F11   F10   F9    F8    FREQ HI  Write 
D402     PW7   PW6   PW5   PW4   PW3   PW2   PW1   PW0   PW LO    Write 
D403     -     -     -     -     PW11  PW10  PW9   PW8   PW HI    Write 
D404     Noise Pulse ///   /\/\  TEST  RING  SYNC  GATE  CONTROL  Write 
D405     ATK3  ATK2  ATK1  ATK0  DCY3  DCY2  DCY1  DCY0  ATK/DCY  Write 
D406     STN3  STN2  STN1  STN0  RLS3  RLS2  RLS1  RLS0  STN/RLS  Write 

Voice 2: 

D407     F7    F6    F5    F4    F3    F2    F1    F0    FREQ LO  Write 
D408     F15   F14   F13   F12   F11   F10   F9    F8    FREQ HI  Write 
D409     PW7   PW6   PW5   PW4   PW3   PW2   PW1   PW0   PW LO    Write 
D40A     -     -     -     -     PW11  PW10  PW9   PW8   PW HI    Write 
D40B     Noise Pulse ///   /\/\  TEST  RING  SYNC  GATE  CONTROL  Write 
D40C     ATK3  ATK2  ATK1  ATK0  DCY3  DCY2  DCY1  DCY0  ATK/DCY  Write 
D40D     STN3  STN2  STN1  STN0  RLS3  RLS2  RLS1  RLS0  STN/RLS  Write 

Voice 3: 

D40E     F7    F6    F5    F4    F3    F2    F1    F0    FREQ LO  Write 
D40F     F15   F14   F13   F12   F11   F10   F9    F8    FREQ HI  Write 
D410     PW7   PW6   PW5   PW4   PW3   PW2   PW1   PW0   PW LO    Write 
D411     -     -     -     -     PW11  PW10  PW9   PW8   PW HI    Write 
D412     Noise Pulse ///   /\/\  TEST  RING  SYNC  GATE  CONTROL  Write 
D413     ATK3  ATK2  ATK1  ATK0  DCY3  DCY2  DCY1  DCY0  ATK/DCY  Write 
D414     STN3  STN2  STN1  STN0  RLS3  RLS2  RLS1  RLS0  STN/RLS  Write 

Filter: 

D415     -     -     -     -     -     FC2   FC1   FC0   FC LO    Write 
D416     FC10  FC9   FC8   FC7   FC6   FC5   FC4   FC3   FC HI    Write 
D417     RES3  RES2  RES1  RES0  FILEX FILT3 FILT2 FILT1 RES/FILT Write 
D418     3 OFF HP    BP    LP    VOL3  VOL2  VOL1  VOL0  MODE/VOL Write 

Misc.: 

D419     PX7   PX6   PX5   PX4   PX3   PX2   PX1   PX0   POT X    Read 
D41A     PY7   PY6   PY5   PY4   PY3   PY2   PY1   PY0   POT Y    Read 
D41B     O7    O6    O5    O4    O3    O2    O1    O0    OSC3/RND Read 
D41C     E7    E6    E5    E4    E3    E2    E1    E0    ENV3     Read 

---------------------------------------------------------------------

Noise - Noise (waveform)
Pulse - Pulse (waveform)
///   - Sawtooth (waveform)
/\/\  - Triangle (waveform)
PW    - Pulse Width
ATK   - Attack (ADSR)
DCY   - Decay (ADSR)
STN   - Sustain (ADSR)
RLS   - Release (ADSR)

To be able to hear sound from a voice, it does need a frequency, it needs to have a waveform selected (at least one), and it needs to have its GATE bit set. The GATE bit will enable the ADSR-Envelope Generator, which will ultimately generate a sound.

For creating the basic waveforms I make a class and some member functions to run through. You already have seen the class Channel above in the set up function.

class Channel {

	private:
		float tickTriangle(float vol) {
			float step = ticks / (float)CLOCK_RATE;
			float value = (std::abs(0.5f - fmod((real_freq * step), 1.f)) * 4.0 * (-1.0f) + 1.0f) * vol;
			return value;
		}
		float tickSawtooth(float vol) {
			float step = ticks / (float)CLOCK_RATE;
			float value = fmod((step * real_freq), 1.0f) * vol;
			return value;
		}
		float tickPulse(float vol) {
			float step = fmod((ticks / (float)CLOCK_RATE * real_freq), 1.0f);
			float value = (step <= ((float)pulse_width / 4096.0)) ? -1.0 * vol : 1.0 * vol;
			return value;
		}
		float tickNoise(float vol) {
			float step = ticks / (float)CLOCK_RATE;
			if (((int)(freq * step) / 1) != lfsr_step) {
				lfsr = ((lfsr << 1) & 0b11111111111111111111111) | (((lfsr >> 17) & 0b1) ^ ((lfsr >> 22) & 0b1));
				lfsr_step = (int)(freq * step) / 1;
			}
			u8 value = (((lfsr >> 22) & 1) << 7) | (((lfsr >> 20) & 1) << 6) | (((lfsr >> 16) & 1) << 5) | (((lfsr >> 13) & 1) << 4) | (((lfsr >> 11) & 1) << 3) | (((lfsr >> 7) & 1) << 2) | (((lfsr >> 4) & 1) << 1) | ((lfsr >> 2) & 1);
			return ((value - 128.f) / 255.f) * vol;
		}
		float tickSilence(float vol) {
			return 0.f;
		}

	public: 
		//	config
		u32 ticks = 0;
		bool GATE = false;
		bool SYNC = false;
		bool RING = false;
		bool TEST = false;
		u16 real_freq = 0;						//	Real frequency
		u16 freq = 0x0000;						//	Register value
		u16 pulse_width = 0x0000;				//	Pulse width for Rectangle
		u8 attack = 0x00;
		u8 decay = 0x00;
		u8 sustain = 0x00;
		u8 release = 0x00;
		u8 pot_x = 0x00;
		u8 pot_y = 0x00;
		float ADSR_timer = 0.0f;
		ADSR_MODE ADSR_mode = ADSR_MODE::RELEASE;
		std::vector<float> buffer;
		Channel* syncTo_channel;
		Channel* syncFrom_channel;
		float (Channel::* process)(float) = &Channel::tickSilence;
		//	used for SYNC & RING
		float val = 0.0f;
		float prev_val = 0.0f;

		void tick(float vol) {

			//	handle SYNC (if set)
			val = sin(2 * pi * (ticks / (float)CLOCK_RATE) * real_freq);
			if (syncTo_channel->SYNC && ((prev_val < 0 && val >= 0) || (prev_val <= 0 && val > 0))) {
				syncTo_channel->ticks = 0;
			}
			prev_val = val;

			//	handle actual soundwave
			ticks = (ticks + SAMPLE_STEPS) % CLOCK_RATE;
			float adsr_vol = vol;
			ADSR_timer += ((float)SAMPLE_STEPS / (float)CLOCK_RATE) * 1000.f;
			switch (ADSR_mode)
			{
			case ADSR_MODE::ATTACK:
				adsr_vol = (ADSR_timer / ATTACK[attack]) * vol;
				if (ADSR_timer > ATTACK[attack]) {
					ADSR_mode = ADSR_MODE::DECAY;
					ADSR_timer = fmod(ADSR_timer, ATTACK[attack]);
				}
				break;
			case ADSR_MODE::DECAY:
				adsr_vol = vol - (( vol - ( vol * (sustain / 15.0))) * (ADSR_timer / DECAY[decay]));
				if (ADSR_timer > DECAY[decay]) {
					ADSR_mode = ADSR_MODE::SUSTAIN;
					ADSR_timer = 0.0f;
				}
				break;
			case ADSR_MODE::SUSTAIN:
				adsr_vol = vol * (sustain / 15.f);
				break;
			case ADSR_MODE::RELEASE:
 				adsr_vol = std::fmax((float)sustain - std::fmax((float)sustain * (ADSR_timer / RELEASE[release]), 0.0f), 0.0f) / 15.f;
				break;
			default:
				break;
			}
			
			//	process waveform
			float v = (this->*process)(pow(adsr_vol, 2));		//	pow 2/3 is a close-ish estimate to logarithim increase/decrease in volume
			
			//	handle RING (if set)
			if (RING) {
				v *= syncFrom_channel->val;
			}
			buffer.push_back(v);
			buffer.push_back(v);
		}
		
		void setNPST(u8 val) {
			//	if GATE is set, ATTACK is inited, else RELEASE is inited
			//	This is EDGE-SENSITIVE!
			if (!GATE && val & 0b0001) {
				ADSR_timer = 0.0f;
				ADSR_mode = ADSR_MODE::ATTACK;
			}
			else if(GATE && (val & 0b0001) == 0x0) {
				ADSR_timer = 0.0f;
				ADSR_mode = ADSR_MODE::RELEASE;
			}
			GATE = val & 0b0001;
			SYNC = val & 0b0010;
			RING = val & 0b0100;
			TEST = val & 0b1000;
			switch ((val >> 4) & 0b1111)
			{
			case 1: 
				process = &Channel::tickTriangle;
				break;
			case 2:
				process = &Channel::tickSawtooth;
				break;
			case 4:
				process = &Channel::tickPulse;
				break;
			case 8:
				process = &Channel::tickNoise;
				break;
			default:
				process = &Channel::tickSilence;
				break;
			}
			//	if TEST is set, TICKS are reset, so the oscillator starts fresh
			if (TEST) {
				ticks = 0;
			}
		}

		void setAttackDecay(u8 val) {
			attack = (val >> 4) & 0b1111;
			decay = val & 0b1111;
		}

		void setSustainRelease(u8 val) {
			sustain = (val >> 4) & 0b1111;
			release = val & 0b1111;
		}

		void setFreqLo(u8 val) {
			freq = (freq & 0b1111111100000000) | val;
			real_freq = floor(freq * 0.0596);				//	set real frequency
		}

		void setFreqHi(u8 val) {
			freq = (freq & 0b0000000011111111) | (val << 8);
			real_freq = floor(freq * 0.0596);				//	set real frequency
		}

		void setPulseWidthLo(u8 val) {
			pulse_width = (pulse_width & 0b1111111100000000) | val;
		}

		void setPulseWidthHi(u8 val) {
			pulse_width = (pulse_width & 0b0000000011111111) | ((val & 0b1111) << 8);
		}

		void setSyncToChannel(Channel* channel) {
			syncTo_channel = channel;
		}

		void setSyncFromChannel(Channel* channel) {
			syncFrom_channel = channel;
		}
};

ADSR – Attack, Decay, Sustain, Release

Attack, Decay, Sustain, Release : ADSR Explained - MakeMusic!
ADSR

The ADSR-Envelope generator can be used to create lots and lots of sounds, each with its own ring to it, its own characteristics. The ADSR gives you the freedom to choose values for the individual phases:

Attack

The Attack phase is the phase, where the amplitude rises from 0 to volume (set by writes to 0xD418). The value written to the attack part of the register dictates how long it will take to reach the maximum value (here volume).
(The lengths will be shown in a table a bit below)
It is activated by the GATE bit switched on. This is an edge-sensitive event.

Decay

The Decay phase is the phase where the amplitude will fall, from the volume level, to the sustain level. The value written to the decay part of the register dictates how long it will take to reach the sustain level. This phase starts as soon as the Attack phase ends.

Sustain

The Sustain phase is the phase where the current volume is held, for as long as the code dictates us. The value written to the sustain part of the register dictates the amplitude in the sustain phase (not the duration). This phase starts as soon as the Decay phase ends.

Release

The Release phase is the phase, where the volume, starting at sustain level, decreases to zero over time. The value written to the release part of the register dictates how long it will take to reach zero.
This phase starts as soon as the GATE bit is set to zero. This is an edge-sensitive event.

Timing

VALUE    ATTACK RATE      DECAY/RELEASE RATE
          Time/Cycle              Time/Cycle 
- ------------------------------------------  
0               2 ms                    6 ms  
1               8 ms                   24 ms  
2              16 ms                   48 ms  
3              24 ms                   72 ms  
4              38 ms                  114 ms  
5              56 ms                  168 ms  
6              68 ms                  204 ms  
7              80 ms                  240 ms  
8             100 ms                  300 ms  
9             240 ms                  750 ms 
10            500 ms                   1.5 s 
11            800 ms                   2.4 s 
12               1 s                     3 s 
13               3 s                     9 s 
14               5 s                    15 s 
15               8 s                    24 s 

Additional settings

Pulse width

When selecting the waveform Pulse, we need to make an additional setting to the frequency, and that is the pulse width, or also called duty cycle. Basically, this will tell the waveform, how much of it is supposed to be one / positive amplitude.
Since we have a 12 bit register for the pulse width, we can set the values very precise from 0 to full_length.

Pulse Width / Duty Cycle

In the above image you can see a few settings of the Pulse Width and how they look like in the actual wave. Notice that the frequency is always the same, and it’s only the Pulse Width that is changed.

TEST-Bit

When writing 1 to the TEST-bit, the current cycle will be reset. That is all that it does.

//	if TEST is set, TICKS are reset, so the oscillator starts fresh
if (TEST) {
	ticks = 0;
}

SYNC-Bit

When enabled, the current voice will be hard-synched to it’s SYNC-channel / oscillator. On voice 1 it will be synched to voice 3, on voice 2 it will be synched to voice 1, and on voice 3 it will be synched to voice 2.
Hard-Synching means, whenever the sine-wave of the frequency of the SYNC-channel completes a cycle, the current voice is reset.

//	handle SYNC (if set)
val = sin(2 * pi * (ticks / (float)CLOCK_RATE) * real_freq);
if (syncTo_channel->SYNC && ((prev_val < 0 && val >= 0) || (prev_val <= 0 && val > 0))) {
	syncTo_channel->ticks = 0;
}
prev_val = val;
SYNC-bit / hard-synching

In the image you can see the SYNC-channel (e.g. voice 3, labelled Master) and the current channel (e.g. voice 1, labelled Slave). Whenever the SYNC-Channel completes a cycle, the current voice is reset. This can be used to create some unique sounds.
As you may already have noticed, this only works when there is a frequency set to the SYNC-channel. The frequency should be different than in the current channel, or you will not be able to hear any effect at all.

;*** TEST for SYNC bit ***

;*** Startadresse
*=$0801
 
;*** BASIC-Zeile
 !word main-2
 !word 2018
 !text $9E," 2062",$00,$00,$00

 !zone main
  main
  lda #$0f    ; set volume
  sta $d418
  
  ; channel 1
  
  lda #$a4    ; set freq lo
  sta $d400
  
  lda #$06    ; set freq hi
  sta $d401
  
  lda #$c4    ; set pw lo
  sta $d402
  
  lda #$16    ; set pw hi
  sta $d403
  
  lda #$11    ; set attack / decay
  sta $d405
  
  lda #$96    ; set sustain / release
  sta $d406
  
  lda #$13    ; set pulse, sync and gate
  sta $d404
  
  ; channel 3
  lda #$a4    ; set freq lo
  sta $d40e
  
  lda #$02    ; set freq hi
  sta $d40f
  
  lda #$01
  oops
  bne oops

The above code can test your implementation of the SYNC bit. Your output should be similar to the following:

SID SYNC-Bit Test

RING-Bit

When enabled, the RING-Bit will apply ring-modulation to the current voice, also again with its SYNC-channel (same as before). Ring-modulation is nothing but multiplying the current voice with the sine-wave of the SYNC-channel. Again, the SYNC-channel needs to have a set frequency, other than the current voice, to create an audible effect.

//	handle RING (if set)
if (RING) {
	v *= syncFrom_channel->val;
}
Ringmodulation
;*** TEST for RING-Bit ***

;*** Startadresse
*=$0801
 
;*** BASIC-Zeile
 !word main-2
 !word 2018
 !text $9E," 2062",$00,$00,$00

 !zone main
  main
  lda #$0f    ; set volume
  sta $d418
  
  lda #$c4    ; set freq lo
  sta $d400
  
  lda #$16    ; set freq hi
  sta $d401
  
  lda #$c9    ; set ring freq lo
  sta $d40e
  
  lda #$00    ; set ring freq hi
  sta $d40f
  
  lda #$33    ; set attack / decay
  sta $d405
  
  lda #$9f    ; set sustain / release
  sta $d406
  
  lda #$15    ; set channel and gate, and RING
  sta $d404
  
  wait
  bne wait

The above code can test your implementation of the RING bit. Your output should be similar to the following:

SID RING-Bit test

Waveform tests

To be able to compare the emulation implementation to other emulators or even a real C64, I created a few tests with constant tones.

Triangle

;*** Triangle Constant Tone ***

;*** Startadresse
*=$0801
 
;*** BASIC-Zeile
 !word main-2
 !word 2018
 !text $9E," 2062",$00,$00,$00

 !zone main
  main
  lda #$08    ; set volume
  sta $d418
  
  lda #$c4    ; set freq lo
  sta $d400
  
  lda #$16    ; set freq hi
  sta $d401
  
  lda #$0f    ; set pulse width
  sta $d402
  sta $d403
  
  lda #$69    ; set attack / decay
  sta $d405
  
  lda #$fc    ; set sustain / release
  sta $d406
  
  lda #$11    ; set triangle channel and gate
  sta $d404
  
  oops
  bne oops
Triangle waveform

Sawtooth

;*** Sawtooth Constant Tone ***

;*** Startadresse
*=$0801
 
;*** BASIC-Zeile
 !word main-2
 !word 2018
 !text $9E," 2062",$00,$00,$00

 !zone main
  main
  lda #$08    ; set volume
  sta $d418
  
  lda #$c3    ; set freq lo
  sta $d400
  
  lda #$0a    ; set freq hi
  sta $d401
  
  lda #$69    ; set attack / decay
  sta $d405
  
  lda #$fc    ; set sustain / release
  sta $d406
  
  lda #$21    ; set sawtooth channel and gate
  sta $d404
  
  oops
  bne oops
Sawtooth waveform

Pulse

;*** Pulse Constant Tone ***

;*** Startadresse
*=$0801
 
;*** BASIC-Zeile
 !word main-2
 !word 2018
 !text $9E," 2062",$00,$00,$00

 !zone main
  main
  lda #$08    ; set volume
  sta $d418
  
  lda #$c4    ; set freq lo
  sta $d400
  
  lda #$09    ; set freq hi
  sta $d401
  
  lda #$0f    ; set pulse width
  sta $d402
  sta $d403
  
  lda #$69    ; set attack / decay
  sta $d405
  
  lda #$fc    ; set sustain / release
  sta $d406
  
  lda #$41    ; set pulse channel and gate
  sta $d404
  
  ldx #$69
  xloop
  bne xloop
Pulse waveform

Noise

;*** Noise Constant Tone ***

;*** Startadresse
*=$0801
 
;*** BASIC-Zeile
 !word main-2
 !word 2018
 !text $9E," 2062",$00,$00,$00

 !zone main
  main
  lda #$08    ; set volume
  sta $d418
  
  lda #$c4    ; set freq lo
  sta $d400
  
  lda #$09    ; set freq hi
  sta $d401
  
  lda #$69    ; set attack / decay
  sta $d405
  
  lda #$fc    ; set sustain / release
  sta $d406
  
  lda #$81    ; set noise channel and gate
  sta $d404
  
  ldx #$69
  xloop
  bne xloop

The noise channel is a 23-bit LFSR, that on shifting left, fills bit 0 with bit 17 XOR’ed with bit 22. You can see the implemenation at the beginning of the page, where all the waveforms are implemented. The output is generated from bits 22, 20, 16, 13, 11, 7, 4 and 2. You may be seeing other docs that tell you other output bits (e.g. 20, 18, 14, 11, 9, 5, 2 and 0) – those are wrong, and will give you a false distribution of numbers in the output of the LFSR.

Distribution of randomness: correct output bits (left) vs wrong output bits (right)
Noise waveform

Comments

Leave a Reply