Skip to content

Pulse Width Modulation

Changing a square wave's shape while it plays

A pulse wave's tone depends on how much of each cycle it spends high. The C64's SID sets that with a 12-bit number per voice, and music players changed it every frame to make notes move; Rob Hubbard's first player swept it between a square and a narrow pulse. The NES offers four fixed shapes, and Spectrum programmers used pulse width on the one-bit beeper as a volume control.

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.

Assembly
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 = narrowing

The 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 $FFF to $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): Vxx and 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 $20 from $0800 to $0E00 in 48 frames and back.

See also

Checked. Verified against primary sources and signed off by a person.