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):
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 loopWe 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):
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 phaseWe 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):
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):
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 DMAWe 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_cueand 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
- Sinclair, ZX Spectrum BASIC Programming, chapter 23: port
$FE, speaker, MIC and border bits. - MOS Technology, 6581 SID datasheet, register descriptions: waveform bits, ADSR and filter routing.
- NESdev’s hardware research: APU channels, pulse, sweep and DMC.
- Commodore, Amiga Hardware Reference Manual, Audio Hardware: waveform data, length, volume, period and DMA.
Related
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.