Code status: assembled and emulator-executed. PAL C64, started with RUN through the ROM, in Emu198x. The recorded checks identify the tested sources and observations. They establish the stated cases on that configuration; original-hardware checks are separate.
A gate, not a switch
Each of the SID’s three voices has a gate bit, bit 0 of its control register. Setting it starts the voice’s envelope: attack up to peak, decay down to the sustain level, then sustain for as long as the gate stays set. Clearing it starts release, a fall to silence at the release rate. The envelope alone sets how loud the voice is, so the waveform can stay selected after the note ends.
None of those phases is instant. Even the fastest rate, 0, takes milliseconds. This example targets a PAL C64, loaded at $0801 and started with RUN.
The routines’ contract
Before including the file, set SID_VOICE to the voice’s first register: $D400 for voice 1, $D407 for voice 2, $D40E for voice 3. Include it once, for one voice.
The routines write only that voice’s seven registers. They never read the SID back: registers $D400–$D418 are write-only. Master volume and filter routing ($D415–$D418) belong to the caller, and so do the other two voices. If an interrupt handler also writes this voice, the caller has to stop the two interleaving; these routines don’t arbitrate.
Each routine changes A and the flags. X and Y are preserved.
; Start and stop a note on one SID voice.
;
; Before including this file, set SID_VOICE to the voice's first register:
; $D400 for voice 1, $D407 for voice 2, $D40E for voice 3.
; Include it once; it defines one set of routines for that voice.
;
; The routines write only SID_VOICE+0 to SID_VOICE+6 and never read a SID
; register ($D400-$D418 are write-only). Master volume, filter routing and
; the other voices ($D415-$D418 and the remaining voice registers) belong
; to the caller. If an interrupt handler also writes this voice, the caller
; must stop the two from interleaving; these routines do not arbitrate.
;
; Each routine changes A and the flags and preserves X and Y.
SID_WAVE_PULSE = $40
SID_GATE = $01
; A = attack/decay ($D405 layout), X = sustain/release ($D406 layout).
; Selects the pulse waveform at 50% width with the gate clear. If the voice
; was gated, clearing the gate starts its release.
sid_voice_setup:
sta SID_VOICE+5
stx SID_VOICE+6
lda #$00
sta SID_VOICE+2
lda #$08
sta SID_VOICE+3
lda #SID_WAVE_PULSE
sta SID_VOICE+4
rts
; A = frequency high byte, X = frequency low byte.
; Clears the gate, then sets it, so a new attack begins even if the voice
; was still gated. The attack rises from the envelope's current level,
; not from zero.
sid_note_on:
stx SID_VOICE+0
sta SID_VOICE+1
lda #SID_WAVE_PULSE
sta SID_VOICE+4
lda #SID_WAVE_PULSE | SID_GATE
sta SID_VOICE+4
rts
; Clears the gate. Release falls from the current level at the release
; rate. The waveform stays selected; the envelope silences the voice.
sid_note_off:
lda #SID_WAVE_PULSE
sta SID_VOICE+4
rtssid_voice_setup takes the attack/decay byte in A and the sustain/release byte in X, in the same layout as the chip’s registers: attack and sustain in the high nibble. It selects a pulse wave at 50% width with the gate clear.
sid_note_on takes the frequency high byte in A and the low byte in X. It writes the frequency, clears the gate, then sets it. sid_note_off clears the gate and leaves the pulse waveform selected.
Starting a note again
The data sheet describes the sequence this routine relies on. If the gate is cleared before the envelope has finished, release begins from whatever level it had reached. If the gate is then set again before release reaches zero, a new attack begins from that level, not from zero.
The data sheet doesn’t promise that writing an already-set gate starts another attack. So sid_note_on always clears the gate first. Two writes, a few cycles apart, are enough to start a new attack on a voice that is still sounding.
A note restarted this way doesn’t begin from silence. Music drivers often clear the gate a frame or two before the next note so the envelope can fall first, a technique called “hard restart”.
A note is sustaining at level $88 when sid_note_on is called again with attack 0. Where does the new attack start?
Show the explanation
At about $88, the level the envelope had reached. It climbs to peak from there, then decays back to the sustain level. The verification run saw exactly that on voice 3: the first sample after the retrigger read $8B.
Envelope rates
Attack, decay and release each take a 4-bit rate. The data sheet’s Table 2 gives these times for a 1 MHz clock. Attack is the time to rise from zero to peak. Decay and release are the time to fall from peak to zero.
| Value | Attack | Decay / Release |
|---|---|---|
| $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 | 250 ms | 750 ms |
| $A | 500 ms | 1.5 s |
| $B | 800 ms | 2.4 s |
| $C | 1 s | 3 s |
| $D | 3 s | 9 s |
| $E | 5 s | 15 s |
| $F | 8 s | 24 s |
For other clocks, multiply by 1 MHz divided by the clock. A PAL C64 runs at about 0.985 MHz, so each time is about 1.5% longer. Decay and release fall from the current level, so they finish sooner when they start below peak.
The demo uses attack 0, decay 0, sustain 15 and release 9. Sustain 15 is peak, so decay has nowhere to fall and the note holds at full level. After the gate clears, release 9 takes about three quarters of a second to fall from peak to zero.
In the verification run on Emu198x, attack 0 took 2.28 ms to reach peak and release 9 reached zero within 738 ms. The first attack also began 32.6 ms after the gate was set. That emulator models the envelope’s internal rate counter after reSID, and that counter sometimes has to wrap before a new rate takes effect. We haven’t measured this delay on a real C64, so don’t time game events to the gate write.
The frequency value
The 16-bit frequency value sets the oscillator’s pitch:
Fout = Fn × Fclk / 16777216Fclk is the system clock: 0.98525 MHz on PAL, 1.02273 MHz on NTSC. To find Fn, rearrange: Fn = Fout × 16777216 / Fclk. Middle C (261.63 Hz) on PAL gives 4455, or $1167. The same value on NTSC plays about 3.8% sharp, so NTSC programs need their own table.
Cost
The assembler’s listing counts each routine from its first instruction through RTS. Add 6 cycles for the JSR that calls it.
| Routine | Bytes | Cycles through RTS |
|---|---|---|
sid_voice_setup |
22 | 32 |
sid_note_on |
17 | 26 |
sid_note_off |
6 | 12 |
These are instruction costs, not elapsed time. The verification script checks the listing against these figures. Once a note is gated, the SID runs the envelope on its own; the processor needs to do nothing more until the next gate change.
Run the complete example
The demonstration sets master volume 15 with the filter off, sets up voice 1, waits half a second, then plays middle C. It holds the note for 25 frames (half a second on PAL), clears the gate and waits for the release.
; SID note trigger demonstration: one note on voice 1.
; Target: C64 PAL, loaded at $0801 and started with RUN.
* = $0801
!byte $0b, $08, $0a, $00, $9e ; 10 SYS 2064
!text "2064"
!byte $00, $00, $00
* = $0810
SID_VOICE = $d400 ; voice 1
start:
lda #$0f ; volume 15, no filter, voice 3 audible
sta $d418
lda #$00 ; attack 0, decay 0
ldx #$f9 ; sustain 15, release 9
jsr sid_voice_setup
ldy #25 ; half a second of silence first
jsr wait_frames
lda #$11 ; C4 on PAL: $1167
ldx #$67
jsr sid_note_on
ldy #25 ; hold for half a second
jsr wait_frames
jsr sid_note_off
ldy #50 ; let the release finish
jsr wait_frames
lda #1
sta done
finished:
jmp finished
; Wait Y frames (Y = 1-255), timing each frame by raster line 255.
wait_frames:
lda $d012
cmp #$ff
bne wait_frames
wait_line_end:
lda $d012
cmp #$ff
beq wait_line_end
dey
bne wait_frames
rts
!source "sid-note.inc"
done:
!byte 0Save demo.asm and sid-note.inc together and build with:
asm198x --dialect acme --prg demo.asm -o demo.prgLoad demo.prg into a PAL C64 emulator and type RUN. You should hear one note that fades over most of a second. In Emu198x, writing $D418 makes a short click before the note.
Try changing ldx #$f9 to ldx #$89. With sustain 8, decay now has somewhere to fall: the note settles at about half its peak level.
The sample’s source and verification recipe includes a Makefile and an executable check. The check boots Emu198x’s PAL C64 from its ROMs, types RUN, and checks four things:
- which bytes the routines write, and that X and Y survive;
- the attack, release and retrigger timings on voice 3’s envelope;
- the demo’s pitch, hold and release in captured audio;
- the listing costs above.
These establish emulator behaviour. Listening and original-hardware checks are separate.
Related
Patterns: SID Voice Setup, SID Multi-Voice
Vault: The SID Chip | Commodore 64
Sources
MOS Technology, 6581 Sound Interface Device (SID) data sheet: the register map (write-only voice registers, read-only ENV3), the gate bit, envelope behaviour on re-gating, the frequency equation, and Table 2’s envelope rates with the clock-scaling note. Commodore, Commodore 64 Programmer’s Reference Guide (1983), chapter 6: PAL and NTSC system frequencies. Hard restart: Cadaver, “Building a Musicroutine”, codebase64. Timings marked as Emu198x observations come from the sample’s verification run, not from hardware.