Skip to content

Four Ways to Make a Sound

Compare CPU-driven beeper sound, NES channels, SID synthesis and Amiga samples, with their timing and ownership costs.

Code status: illustrative code. Adapt the listing to your program and check it on your target machine. This page links no assembly or execution record for the listing itself.

Overview

The tune sequencer introduces a reusable idea: store events and advance through them. That does not make the drivers interchangeable. Their timing, voice state and sound-generation work depend on the target. We can compare where each machine does the work, which resources a cue needs and what continues after our routine returns.

Four approaches place the work in different parts of the machine:

  • Spectrum 16K/48K — CPU-driven beeper. The CPU changes the speaker output through the ULA. Repeating those changes produces a tone; the spacing of the edges determines its waveform and pitch. The 128K also has an AY sound chip.
  • NES — specialised channels. Two pulse channels, triangle, noise and DMC sample playback each have different controls. We arrange sounds around those differences.
  • C64 — oscillators and envelopes. Each of the SID’s three voices has waveform selection and an ADSR envelope. Selected voices can feed a shared analogue filter; synchronisation and ring modulation connect particular oscillators.
  • Amiga — sample playback. Four audio channels stream signed 8-bit sample data from Chip RAM using DMA. We can supply recorded sounds or draw our own waveforms, within memory and playback-rate limits.

The comparison

Machine We provide The hardware provides Cost or constraint to keep visible
Spectrum beeper Timed output changes ULA output and speaker circuitry CPU time for each edge; shared border/MIC port
Spectrum 128K AY Tone periods, mixer settings, levels Three tone generators, shared noise and envelope generators Shared generators constrain independent instruments
NES APU Channel-specific settings and DPCM data Two pulse, triangle, noise and DMC channels Different controls per channel; DMC DMA costs
C64 SID Frequency, waveform, ADSR and filter settings Three oscillators/envelopes and a shared analogue filter Filter ownership; oscillator relationships for sync/ring modulation
Amiga Paula Sample data, address, length, period and volume Four DMA-fed audio channels Chip RAM, bus use, looping and fixed stereo routing

Hardware-generated audio can continue while the CPU does other work. That does not make the whole music player free: someone still advances notes, applies effects and decides which sound owns each resource.

Starting a sound, four ways

The comparison above and the completion question below can be explored without reading assembly. Open a fragment when we know that machine’s registers and instructions; we do not need to learn all four syntaxes together.

These are illustrative fragments, not complete programs or four versions of the same note. Startup, duration, stopping and resource ownership belong to the surrounding program. Read each fragment’s assumptions before adapting it.

Inspect the Spectrum fragment

Spectrum — the CPU is the oscillator (Sound Beep):

Z80 assembly
    ld   b,50          ; 50 complete high/low pairs
loop:
    ld   a,$10
    out  ($fe),a       ; speaker bit high, black border, MIC bit clear
    call delay         ; supplied delay routine must preserve B
    xor  a
    out  ($fe),a       ; speaker bit low, same border and MIC bits
    call delay
    djnz loop

We supply delay and a valid stack. The whole loop, including calls and branches, determines the edge spacing; it is not just the delay routine. Changing the delay changes pitch, while B counts waveform cycles rather than milliseconds. A longer period also makes these 50 cycles last longer. Interrupt work and contention can disturb the timing. This loop occupies the CPU until it ends and leaves the speaker bit low. A coloured-border game must preserve its border bits in both output values.

Inspect the NES fragment

NES — configure pulse channel 1 (A Square Wave):

6502 assembly
    lda #$01
    sta $4015          ; enable pulse 1; disable other channels
    lda #%10111111
    sta $4000          ; 50% duty, length halt, constant volume 15
    lda #$08
    sta $4001          ; sweep disabled, negate set to avoid upward overflow mute
    lda #$fd
    sta $4002          ; timer low byte
    lda #$00
    sta $4003          ; timer high bits 0, load length, restart pulse phase

We assume initialised APU/frame-interrupt handling and exclusive ownership of the channels affected by $4015. The timer is $0FD; it gives approximately 440 Hz on NTSC, with different tuning on PAL. The non-zero length load permits playback, and the halt bit prevents that counter from expiring. The tone continues after these writes. For this isolated example, writing zero to $4015 stops it; in a game we must preserve other channels’ enable bits.

The pulse channels offer duty choices and volume envelopes. Triangle has neither a programmable duty cycle nor a volume envelope. Noise has selectable periods and sequence modes rather than a musical note timer. DMC reads delta-encoded sample data and can steal CPU cycles for its memory fetches. A shared “pitch and volume” interface cannot describe all five faithfully.

Inspect the C64 fragment

C64 — give a pulse voice an envelope (SID Note Trigger):

Assembly
    lda #$00
    sta $d404          ; gate off before this trigger
    sta $d417          ; no voices routed through the shared filter
    sta $d402          ; pulse-width low byte
    lda #$08
    sta $d403          ; pulse width $800: half of the oscillator cycle
    lda #$0f
    sta $d418          ; master volume 15, filter modes off
    lda #$67
    sta $d400
    lda #$11
    sta $d401          ; frequency $1167: approximately middle C on PAL
    lda #$09
    sta $d405          ; attack 0, decay 9
    lda #$f9
    sta $d406          ; sustain 15, release 9
    lda #$41
    sta $d404          ; pulse waveform ($40) + gate on ($01)

We assume SID registers are visible through the C64’s memory mapping and the program owns the SID; other voices are already silent. The filter routing and master volume writes affect shared resources. $D405 supplies attack/decay and $D406 supplies sustain/release. With sustain at maximum, this example does not demonstrate an audible decay to a lower level. Later, write $40 to $D404 to release the note while retaining its waveform. Gate-off starts release; it does not promise instant silence or reset the envelope to zero.

Sawtooth would use $21 for waveform plus gate. The pulse-width registers affect pulse, not sawtooth. We can keep the frequency unchanged and compare their tone colour, but the result is not the same instrument as an NES pulse voice.

Inspect the Amiga fragment

Amiga — hand the audio channel a waveform (Playing a Sample):

68000 assembly
    move.l  #sample,AUD0LC(a5)
    move.w  #sample_words,AUD0LEN(a5) ; length in 16-bit words
    move.w  #428,AUD0PER(a5)          ; spacing between output samples
    move.w  #64,AUD0VOL(a5)           ; full channel volume
    move.w  #$8201,DMACON(a5)         ; set master DMA and channel 0 DMA

We assume a5 points at the custom registers, the named offsets are defined, channel 0 DMA is initially off, audio modulation is disabled, and the program has safely obtained hardware ownership. Under AmigaOS, use its audio facilities rather than taking registers away from another owner. sample must be word-aligned in Chip RAM, with an even number of signed 8-bit samples; sample_words is half that byte count and must fit the length register. Keep the memory valid while DMA can read it.

Paula repeats the configured block unless we arrange a different continuation or stop. Period 428 specifies a playback rate, not an absolute musical pitch: a short repeating waveform and a recording of a whole note can sound very different at that rate. Lowering the period plays the data faster, raising pitch and shortening one traversal of the sample block. A looping cue can still have a separately timed overall duration in software. DMA saves per-sample CPU writes but still uses memory bandwidth; arrangement, effects and channel ownership still require software.

Choosing a sound that communicates

For a collected item, we might use a short beeper tone, a bright NES pulse, a SID note with a quick attack, or a small Amiga sample. Those can communicate the same success without having identical waveforms.

Compare a sharp attack with a slower one. Does the cue still feel connected to the collection? Compare a short tail with a long one. Do repeated collections remain distinct, or do they obscure one another? Keep the visible collection feedback too, so the information does not depend on hearing the sound.

Our creative choices follow the available controls. On triangle we cannot simply turn down an amplitude envelope. On the beeper, delaying waveform work changes the sound itself. On Paula, playing a sample faster changes both its pitch and the time to traverse its data. Understanding those differences helps us choose an effect that works in the game.

When a shared operation stops fitting

Consider an operation called play_cue that takes a cue number. In our first memory game it returns after the cue finishes. The caller then restores the panel and checks input. That is a useful, simple promise.

Before tracing, write down two separate moments: when the call returns and when the cue finishes. They coincide in the waiting version.

Now we want another implementation that starts hardware playback and returns while it continues. Predict what the unchanged caller does: does the panel stay active until the sound ends? When can it read input again?

Inspect the mismatch

The caller can restore the panel before the sound finishes. Input can be checked sooner, but the old promise has changed. The same operation name and argument did not preserve the meaning of completion.

We have choices:

  • Keep a separate waiting routine for the small memory game, accepting its delay.
  • Expose start_cue and a completion check when the game needs other work to continue.
  • Keep different playback routines and share only the event’s meaning, such as “choice accepted”, when their lifetimes and capabilities differ too much.

For each choice, name who advances the sound, who owns its channel or CPU time, and what happens if another cue arrives. A completion check alone does not make a CPU-driven beeper play in the background.

A cue identifier need not promise an identical instrument. SID envelopes, NES channel-specific controls and Amiga sample data are not interchangeable parameters. Keep those choices visible in each implementation. Compare whether the cue communicates the same event, then account separately for its sound, storage and execution cost.

Hardware references

Patterns: Tune Sequencer — event data and playback updates, with target-specific contracts · foundational audio: Sound Beep (Spectrum) · A Square Wave (NES) · SID Note Trigger (C64) · Playing a Sample (Amiga)

Vault: Paula | APU | The SID Chip | ULA | AY-3-8912

Reuse an effect with an explicit lifetime

Decide who stops the sound as well as who starts it. A blocking tone owns the CPU until it returns. A serviced effect needs its mute routine called at the agreed rate. A queued MOS effect depends on firmware service and queue policy. These entries include native working and omitted-trigger recordings.

Machine Language Implementation
Atom BASIC Blocking tone
Amstrad CPC BASIC MOS queue
Atari 800XL BASIC Blocking tone
Commodore PET BASIC Blocking tone
Commodore VIC-20 BASIC Blocking tone
Dragon 32 BASIC Blocking tone
Mattel Aquarius BASIC Blocking tone
MSX1 BASIC Blocking tone
Oric Atmos BASIC ROM effect
Spectravideo SVI-328 BASIC Blocking tone
Atari 2600 ASSEMBLY Serviced pulse
Atari 5200 ASSEMBLY Serviced pulse
Atari 7800 ASSEMBLY Serviced pulse
ColecoVision ASSEMBLY Serviced pulse
Memotech MTX ASSEMBLY Serviced pulse
Game Boy ASSEMBLY Serviced pulse
Game Gear ASSEMBLY Serviced pulse
Master System ASSEMBLY Serviced pulse
SG-1000 ASSEMBLY Serviced pulse
Sord M5 ASSEMBLY Serviced pulse
Tatung Einstein ASSEMBLY Serviced pulse
Jupiter Ace FORTH Blocking tone
BBC Micro ASSEMBLY MOS queue
Acorn Electron ASSEMBLY MOS queue

ZX80 and ZX81 have no stock programmable speaker. Their input entries state that boundary. The PET example requires a CB2 sound connection or the selected emulated output. Other language and expansion routes remain separate work.

Eight service calls do not automatically mean eight complete elapsed frames: the first decrement may happen in the triggering update. A sound queue, effect priority and music channel sharing are useful next components; a register writer does not supply them.