A pulse wave is a signal that switches between high and low, and its sound depends on how long it stays high. Spend half of each cycle high and you get a square wave, which Commodore’s own SID data sheet calls “bright, hollow”. Narrow the high part and the same note turns “nasal, reedy”. The fraction of the cycle spent high is the duty cycle, and changing it while a note plays is pulse width modulation.
The Commodore 64 made this unusually easy. Its SID sound chip holds each voice’s pulse width in a register, so a program can reshape a note by writing one number. The data sheet points out what happens if you keep doing it: “Sweeping the pulse width in real-time produces a dynamic ‘phasing’ effect which adds a sense of motion to the sound.” C64 music players did exactly that, fifty times a second, and it became the usual way of giving a SID instrument its character. Anthony McSweeney, taking apart Rob Hubbard’s music player in 1993, called it “the most common way” of creating timbre on the chip.
What the duty cycle changes
Every repeating wave can be split into a fundamental (the note you hear) and harmonics at two, three, four times its frequency and so on. For a pulse wave, the duty cycle decides which harmonics are missing:
| Duty cycle | Harmonics missing | Character |
|---|---|---|
| 50% (square) | every even one: 2nd, 4th, 6th… | hollow |
| 25% | every 4th: 4th, 8th, 12th… | reedy |
| 12.5% | every 8th: 8th, 16th… | thin |
| 75% | every 4th, as at 25% | sounds the same as 25% |
The pattern comes from the maths of a pulse wave: with a duty cycle d, the strength of harmonic n goes as sin(nπd)/n, which is zero whenever nd is a whole number. A 75% wave is a 25% wave turned upside down, and the ear hears both the same, so a pulse only ever has half its range of tones: 12.5% sounds like 87.5%. Writing about the Amiga in 1988, Rick Rae described the square wave the usual way: “Devoid of even harmonics, a square wave has a hollow sound reminiscent of a clarinet.”
The SID’s pulse registers
Each SID voice has a 12-bit pulse width split across two registers. The high register uses only its bottom four bits.
| Register | Bits | Function |
|---|---|---|
$D402 |
7-0 | Voice 1 pulse width, low 8 bits |
$D403 |
3-0 | Voice 1 pulse width, high 4 bits |
$D409 |
7-0 | Voice 2 pulse width, low 8 bits |
$D40A |
3-0 | Voice 2 pulse width, high 4 bits |
$D410 |
7-0 | Voice 3 pulse width, low 8 bits |
$D411 |
3-0 | Voice 3 pulse width, high 4 bits |
The data sheet gives the duty cycle as PWn/40.95 per cent, so $800 (2048) is a square wave and values near either end give very narrow pulses. At 0 or 4095 ($FFF), it says, the output becomes “a constant DC output”: no wave and no sound. The registers only matter when the voice is playing the pulse waveform, bit 6 of its control register ($D404 for voice 1).
The Programmer’s Reference Guide lets BASIC users hear the difference for themselves: after setting a square wave with POKE S+3,8, it asks the reader to change the 8 to a 1, a duty cycle of about 6%, and asks, “Notice how dramatic the difference in sound quality is?”
A sweep in machine code
The SID has no automatic sweep. The program has to change the width itself, and the usual clock for that is the screen: once per frame, 50 times a second on a PAL machine. This program, for the ACME assembler, plays a note on voice 1 and sweeps its pulse from a square wave at $0800 to a narrow pulse at $0E00 and back, taking just under a second each way. Run it with SYS 49152; RUN/STOP and RESTORE together stop it.
STEP = $20 ; added to the width each frame
* = $c000
lda #$0f
sta $d418 ; volume 15, filter off
lda #$00
sta $d405 ; attack 0, decay 0
lda #$f0
sta $d406 ; sustain 15, release 0
lda #$45
sta $d400 ; frequency $1D45: about 440 Hz on a PAL C64
lda #$1d
sta $d401
jsr store ; start at $0800, a square wave
lda #$41 ; pulse waveform, gate on
sta $d404
frame bit $d011 ; wait for the raster to pass line 255
bpl frame ; (bit 7 of $D011 is raster bit 8)
lda dir
bne down
clc ; widen: add STEP
lda pw
adc #STEP
sta pw
lda pw+1
adc #0
sta pw+1
cmp #$0e ; reached $0E00?
bne store2
inc dir ; yes: turn round
bne store2 ; (always taken)
down sec ; narrow: subtract STEP
lda pw
sbc #STEP
sta pw
lda pw+1
sbc #0
sta pw+1
cmp #$08 ; back to $0800?
bne store2
lda pw
bne store2
dec dir ; yes: turn round
store2 jsr store
wait bit $d011 ; wait for the next frame to begin
bmi wait
jmp frame
store lda pw
sta $d402 ; pulse width, low 8 bits
lda pw+1
sta $d403 ; pulse width, high 4 bits
rts
pw !word $0800
dir !byte 0 ; 0 = widening, 1 = narrowingThe program keeps its own copy of the width in pw, because the SID’s registers can be written but not read back. It waits for the raster to reach the bottom of the screen by watching bit 7 of $D011, which is set for the whole of lines 256 to 311; waiting for one exact line can miss it when the KERNAL’s keyboard interrupt runs at the wrong moment. And it turns round at fixed limits instead of letting the 12-bit value wrap. Lasse Öörni (Cadaver) explains why that matters: “if pulsewidth crosses over its whole range from $fff back to $000 an ugly sound is heard”.
How music players did it
Rob Hubbard’s first music player, which Anthony McSweeney disassembled from Monty on the Run for C=Hacking in 1993, shows the technique in its simplest form. McSweeney found the same routine in “Rob’s first 20 or 30 musics”, including Thing on a Spring, Commando, Thrust and International Karate. Each instrument is eight bytes, and byte 6 is the “pulse speed”: its low five bits set how many frames to wait between steps, and its top three bits the size of each step. The routine sweeps the width up to $0Exx and back down to $08xx, from a square wave to what McSweeney calls “a very rectangular” one, and reverses at each end. The current width is stored back into the instrument data itself.
Hubbard had built his own tools for this. In October 1985 Zzap!64 reported that his music utility had been “perfected over the last four months” and that he “constantly makes changes to it so he can customize sounds”. Even so, he felt he was “getting near to the limits of the 64 sound chip” and found its three channels “rather limiting”.
Later players made the sweep more flexible. Öörni, writing about how to build a music routine, gives four parameters per instrument: an initial width, a speed, and low and high limits. The player sets the width at the start of each note, or leaves it running on from the previous note, then adds or subtracts the speed every frame and changes direction at the limits. His own tracker, GoatTracker, goes further and gives each instrument a pulse table, a short program of steps such as “set pulse value $010 (very thin)”, “for 32 ticks, increase pulse with speed $0040” and “jump back to step 3”, so a sweep can change speed and loop.
| Tracker | Command | What it does |
|---|---|---|
| GoatTracker (C64) | 9xx |
Point the voice at a different pulse table, or stop pulse changes with 00 |
| GoatTracker (C64) | pulse table rows | Set a width, or step it by a signed speed for a number of ticks |
| FamiTracker (NES) | Vxx |
Choose one of the four duty cycles, 00 to 03 |
NES: four fixed shapes
The NES’s two pulse channels cannot sweep their width at all. Two bits of $4000 (or $4004 for the second channel) choose one of four eight-step patterns:
| Setting | Pattern | Duty |
|---|---|---|
| 0 | 0 1 0 0 0 0 0 0 |
12.5% |
| 1 | 0 1 1 0 0 0 0 0 |
25% |
| 2 | 0 1 1 1 1 0 0 0 |
50% |
| 3 | 1 0 0 1 1 1 1 1 |
75% (the 25% pattern inverted) |
Trackers such as FamiTracker call the last setting 75%; the NESdev community reference calls it “25% negated”. Either way it sounds the same as setting 1, so an NES composer has three tones on each pulse channel. The only way to change a note’s shape is to switch between them, which is what FamiTracker’s Vxx command does. The APU entry covers the channels in full.
Spectrum: pulse width as volume
The 48K ZX Spectrum’s speaker is a single bit, so there is no waveform to reshape and no volume control. Beeper musicians found a use for pulse width anyway. The three-channel routine by Tim Follin that Your Sinclair printed in August 1987 plays each note as a stream of short pulses, holding the speaker low for C delay loops and high for 16−C. A narrow pulse sounds quieter than a wide one, and the routine widens C and then narrows it as each note plays, so the notes swell and fade: the width of each pulse does the job of a volume envelope. Beeper Music takes the routine apart instruction by instruction.
Sources
- Commodore 64 Programmer’s Reference Guide (Commodore Business Machines): chapter 4, the pulse width registers in BASIC and “Notice how dramatic the difference in sound quality is?” (p. 193); Appendix O, the 6581 SID data sheet, pulse width formula, the DC output at 0 and 4095, the pulse waveform bit, and the register map, which marks the pulse width registers write-only (pp. 459-462).
- Anthony McSweeney, “Rob Hubbard’s Music: Disassembled, Commented and Explained”, C=Hacking, issue 5, 7 March 1993: the games using the routine, the instrument format, the pulse speed byte and the pulsework code.
- Zzap!64, issue 6, October 1985, p. 17: Monty on the Run review, “Turn the Music Up” box on Rob Hubbard.
- Lasse Öörni (Cadaver), “Building a musicroutine”, Codebase64 wiki (web, secondary): the four pulse parameters and the wrap from
$FFFto$000. https://codebase64.org/doku.php?id=base:building_a_music_routine - Lasse Öörni, GoatTracker v2.72 documentation, section 3.4.2, “Pulsetable”, and the pattern command list (web, secondary). https://sourceforge.net/projects/goattracker2/
- Rick Rae, “Amiga Notes: Digital Music Generation”, Amazing Computing, vol. 3 no. 1, January 1988, p. 51: the square wave’s missing even harmonics.
- “FamiTracker Effects Commands”, Battle of the Bits Lyceum (web, secondary):
Vxxand the four 2A03 duty settings. https://battleofthebits.com/lyceum/View/FamiTracker+Effects+Commands - NESdev Wiki, “APU Pulse” (web, secondary): the four duty sequences, with the fourth given as “25% negated”. https://www.nesdev.org/wiki/APU_Pulse
- Your Sinclair, issue 20, August 1987: Program Pitstop, Tim Follin’s three-channel routine.
- The sweep program was assembled with ACME and run on the Commodore 64 ROMs in an emulated PAL C64, logging every write to the SID: one write per frame, rising in steps of
$20from$0800to$0E00in 48 frames and back.