# Defender — Audio Forensics

**Purpose.** Reconstruct the Williams *Defender* sound program as an algorithm, precisely enough
that a Web Audio / AudioWorklet engineer can regenerate every sound procedurally with the correct
pitch, timbre, envelope and duration. There are no samples to recover: the entire audio output of
*Defender* is a 2 048-byte 6800-family program bit-banging an 8-bit DAC in real time.

**Primary evidence on disk**

| Path | Role |
| --- | --- |
| `_defender-reference/historicalsource-williams-soundroms/VSNDRM1.SRC` | The sound program. `NAM DEFENDER SOUNDS REV. 1.0 BY SAM D 10/80`, 1 162 lines. Cited as `VSNDRM1:<line>`. |
| `_defender-reference/mwenge-defender/src/vsndrm1.src` | Modernised copy of the same file, 1 168 lines. Cited as `vsndrm1:<line>`. |
| `_defender-reference/mwenge-defender/src/defa7.src` | Game-side sound tables, `SNDOUT`, `SNDLD`, `SNDSEQ` |
| `_defender-reference/mwenge-defender/src/defb6.src` | Game-side per-object sound triggers |
| `_defender-reference/mwenge-defender/src/amode1.src` | Attract-mode / hall-of-fame organ trigger |
| `_defender-reference/mwenge-defender/src/romc0.src`, `romc8.src` | Operator audio test, `PLAYB` |
| `_defender-reference/mwenge-defender/src/phr6.src` | PIA equates, macros |
| `_defender-reference/mame-williams/williams.cpp`, `williams_m.cpp` | Sound-board machine config, sound-command latch |

Every claim below carries a confidence label. `INFERRED` means the reasoning is mine and is not
directly evidenced. Nothing labeled `INFERRED` should be treated as documented original behavior.

Companion document: `SOUND_EVENT_MATRIX.md` (one row per command value).

---

## 0. Spec corrections

**A-1. The DAC is 8 bits, not 6.** `MULTI_SOURCE_CONFIRMED`. The 6-bit quantity in this system is
the *command bus from the main CPU*, not the converter. `SETUP` programs PIA side A as eight
outputs — `LDAA #$FF / STAA 0,X  SET SIDE A AS OUTPUTS` (`VSNDRM1:175-176`) — and the program writes
full 8-bit values to it (`LDAA #$FF / STAA SOUND`, `VSNDRM1:266-267`). MAME wires PIA port A
straight into an `MC1408` through the 8-bit `dac_byte_interface`
(`williams.cpp:1563`, `williams.cpp:1581`). See §1.

**A-2. Only five command bits are decoded, not six.** `SOURCE_CONFIRMED`. The main board drives six
lines (PB0–PB5 of its PIA at `$CC02`), but the sound program's IRQ handler masks the inverted byte
with `#$1F` (`VSNDRM1:913`). PB5 is therefore a don't-care on the sound side. See §2.3.

**A-3. Sound commands are active-low.** `MULTI_SOURCE_CONFIRMED`. The game writes `$3E`, `$3D`,
`$24`, `$3F` because the sound program complements the port before decoding (`COMA` at
`VSNDRM1:912`) and the game complements again on the way out (`COMB / ANDB #$3F`, `defa7.src:701-702`).
Sound-code *N* is transmitted as byte `$3F − N`. `$3F` means "no command / release the bus".

**A-4. There is no priority arbitration on the sound board.** `SOURCE_CONFIRMED`. The IRQ handler
reloads the stack pointer and re-enables interrupts on entry (`VSNDRM1:909-911`), so a new command
aborts whatever is playing, mid-sample, unconditionally. All priority logic lives in the *game*
(`SNDPRI`, `defa7.src:709-721`). See §8.

**A-5. `williamssound.cpp` is irrelevant to Defender.** `MAME_CONFIRMED`. That file implements the
later CVSD / NARC / ADPCM / pinball boards only (`williamssound.h:28-34`). Defender's sound hardware
is instantiated inline in `williams_base()`.

**A-6. Defender's music is not in tune with itself at A440.** `INFERRED` (arithmetic is airtight,
the aesthetic conclusion is mine). The organ's note table produces an internally correct equal-
tempered scale, but at the real 894.886 kHz E clock every note lands within a few cents of the note
*one semitone below* its label in the source. The Toccata, labeled in D minor, sounds in C♯ minor.
See §6.8.

---

## 1. The sound board

**1.1 CPU.** `MULTI_SOURCE_CONFIRMED`. Motorola **MC6808**, clocked from a 3.579545 MHz colourburst
crystal with an internal ÷4 (`M6808(config, m_soundcpu, SOUND_CLOCK)` where
`SOUND_CLOCK = XTAL(3'579'545)`, `williams.cpp:1532`, `williams.cpp:1540`, with the MAME comment
"internal clock divider of 4, effective frequency is 894.886kHz"). The bus/E clock is therefore

```
E = 3 579 545 / 4 = 894 886.25 Hz        →  1 cycle = 1.117 464 µs
```

Every timing figure in this document is expressed in E cycles and converted at that rate. The 6808
has 128 bytes of on-chip RAM at `$0000–$007F`; the sound program's `ENDRAM EQU $7F`
(`VSNDRM1:13`) and MAME's `map(0x0000, 0x007f).ram(); // internal RAM`
(`williams.cpp:678`) agree.

A minority of bootlegs substitute a 6802 (`defender_6802snd`, `williams.cpp:1613-1618`) and the
`nextcent` variant runs the sound CPU from a 3.12 MHz crystal (`williams.cpp:1608`), which
transposes every sound down by a factor 3.12/3.579545 = 0.8716 (≈ 2.4 semitones). `MAME_CONFIRMED`.

**1.2 Memory map.** `MULTI_SOURCE_CONFIRMED`.

| Range | Contents | Evidence |
| --- | --- | --- |
| `$0000–$007F` | 128 B internal RAM (stack top `$7F`) | `williams.cpp:678`; `VSNDRM1:13,171` |
| `$0400–$0403` (mirror `+$8000`) | MC6821 PIA | `williams.cpp:679`; `SOUND EQU $400`, `VSNDRM1:11` |
| `$F800–$FFFF` | 2 KB program ROM (`video_sound_rom_1.ic12`) | `williams.cpp:2002`; `ROM EQU $F800`, `VSNDRM1:10` |

MAME declares `map(0xb000, 0xffff).rom()` but only 2 KB is loaded at `$F800`
(`ROM_LOAD("video_sound_rom_1.ic12", 0xf800, 0x0800, CRC(fefd5b48))`). My hand-assembly of
`VSNDRM1.SRC` lands the last data byte at `$FFF7` and the interrupt vectors at `$FFF8–$FFFF`, i.e.
the program fills the 2 KB device **exactly**, with zero slack. `SOURCE_CONFIRMED` — that exact fit
is the strongest available check that the recovered source is the shipped program and that my
instruction sizing (used throughout §5) is correct.

**1.3 The DAC.** `MULTI_SOURCE_CONFIRMED`. PIA port A (PA0–PA7) feeds an **MC1408** 8-bit
multiplying DAC at IC6 (`MC1408(config, "dac", 0)...// mc1408.ic6`, `williams.cpp:1563`;
`m_pia[2]->writepa_handler().set("dac", FUNC(dac_byte_interface::data_w))`, `williams.cpp:1581`).
The converter is **unipolar**: code `$00` is one rail, `$FF` the other, `$80` mid-scale. The
program treats it as unipolar throughout — silence is a *static* code, not code `$80`.

There is **no analogue filter model in MAME** and no filter description in the source. The real
board has a reconstruction/low-pass stage after the DAC. `UNRESOLVED` — see §11, Q1.

**1.4 There is no PIA-driven sample clock.** `SOURCE_CONFIRMED`. `SETUP` writes `#$3C` to CRA
(`VSNDRM1:180-181`), which is `CA2 = output, manual, high` and **CA1 interrupts disabled**. The PIA
timer/handshake facilities are unused. Every DAC write is issued by a delay loop inside a synthesis
routine, so the effective sample rate is *different for every sound and often different for every
sample*. This is the single most important structural fact in the whole program.

**1.5 CB2 is the handshake back to the main board.** `MULTI_SOURCE_CONFIRMED`. `SETUP` writes
`#$37` to CRB (`VSNDRM1:178-179`), whose comment reads `CB2 LOW, IRQ ALLOWED`; the bit field decodes
as CB2 = output, manual, low. MAME's Defender I/O map documents `c80c rom_pia_dataa bit 7 — Hand
Shake from sound board` (`williams.cpp:123`). The sound program never changes CB2 after reset, so
the handshake is effectively a static "sound board alive" level. `INFERRED`: the main game does not
appear to test it during play.

---

## 2. Reset, IRQ, and the command bus

### 2.1 Vectors

`SOURCE_CONFIRMED` (`VSNDRM1:1157-1161`, addresses from my assembly):

| Vector | Address | Target | Target address |
| --- | --- | --- | --- |
| IRQ | `$FFF8` | `IRQ` | `$FCB6` |
| SWI | `$FFFA` | `SETUP` | `$F801` |
| NMI | `$FFFC` | `NMI` | `$FD2F` |
| RESET | `$FFFE` | `SETUP` | `$F801` |

`$F800` itself holds a checksum byte — `FCB $FE` in the historical source (`VSNDRM1:169`),
`FCB $FF` in the mwenge copy (`vsndrm1:174`). This is the **only functional difference between the
two files**; everything else in the diff is comment syntax (`;`), RASM-to-vasm operator spelling
(`!>` → `>>`, `!.` → `&`), `,X` → `0,X`, and `ORG`→`RORG`/`REND` for the RAM overlays.
`SOURCE_CONFIRMED` by structural diff of the two files ignoring comments.

### 2.2 Reset path (`SETUP`, `$F801`)

`SOURCE_CONFIRMED` (`VSNDRM1:170-190`):

1. `SEI`; `LDS #$7F` — stack at top of internal RAM.
2. Clear CRA/CRB bit 2 to reach the DDRs; `DDRA = $FF` (8 DAC outputs), `DDRB = $00` (6 command
   inputs + 2 don't-cares).
3. `CRB = $37` — access ORB, CB1 IRQ **enabled**, CB1 active transition **low→high**, CB2 output low.
4. `CRA = $3C` — access ORA, CA1 IRQ disabled, CA2 output high.
5. `STAA HI` with A still `$3C` — the random shift register's high byte is seeded to `$3C`.
   **`LO` is never initialized**; it powers up with whatever the 6808 RAM contains.
   `SOURCE_CONFIRMED` — see §7.
6. Zero `B2FLG`, `BG1FLG`, `BG2FLG`, `SP1FLG`, `ORGFLG`.
7. `CLI` then `BRA *` — the CPU spins in a two-byte branch-to-self forever. **All work happens in
   the IRQ handler and the handler never executes an `RTI`.**

### 2.3 How a command arrives

`MULTI_SOURCE_CONFIRMED`. Main-board side (`defa7.src:696-704`):

```
SNDOUT  PSHS   A,B,CC
        ORCC   #$FF          ; mask IRQ+FIRQ: the two writes are atomic
        CLR    MAPC          ; select bank 0 so $CC02 is the PIA
        LDA    #$3F
        STA    SOUND         ; SOUND EQU $CC02  (phr6.src:18) — release the bus
        COMB
        ANDB   #$3F          ; DONT TURN ON LEDS
        STB    SOUND         ; assert the command, active low
        PULS   A,B,CC,PC
```

`$CC02` is main-PIA1 port B; `phr6.src:130-134` documents it as `B0-B5 SOUND`, `B6-B7 LEDS`,
`CB2 IRQ`. Bits 6–7 drive the coin-door LEDs and are masked out of the sound command
(`romc8.src:418-421` shows the LED routine `ORB #$3F` before writing, i.e. holding the six sound
lines idle while changing the LEDs).

MAME models the latch as (`williams_m.cpp:162-175`):

```
deferred_snd_cmd_w:  m_pia[A]->portb_w(param);
                     m_pia[A]->cb1_w((param == 0xff) ? 0 : 1);
snd_cmd_w:           ... , data | 0xc0);   // "the high two bits are set externally, and should be 1"
```

So on the sound side the byte read at `$0402` is `(game_byte | $C0)`, and **CB1 is low if and only
if all six command lines are high (`$3F`)**. Writing `$3F` and then the command therefore produces
exactly one low→high CB1 edge, which is the transition CRB was programmed for. Two different
non-`$3F` values written back to back produce **no** edge and **no** second interrupt — which is why
both `SNDOUT` and the diagnostic `PLAYB` (`romc8.src:457-468`) always write `$3F` first.

Sound-side decode (`VSNDRM1:909-913`):

```
IRQ     LDS  #ENDRAM      ; throw away the whole stack, whatever was running
        LDAA SOUND+2      ; read PIA ORB at $0402 — this also clears the CB1 IRQ flag
        CLI               ; NOW ALLOW IRQS  ← re-entrant from this instruction onwards
        COMA              ; INVERT INPUT
        ANDA #$1F         ; MASK GARB
```

Therefore:

```
command C = (~game_byte) & $1F        game_byte = $3F − C   for C in $00..$1F
```

* `C = $00` (`game_byte $3F`) — "no command"; the handler falls straight through to `IRQ3`.
* `C = $01..$1F` — the 31 real sound codes.
* PB5 contributes nothing: `$3E` and `$1E` decode identically. `SOURCE_CONFIRMED`.

Worked examples from the game: `$3E → $01`, `$3D → $02`, `$24 → $1B` (`amode1.src:150-165`),
`$3F → $00`. Diagnostics confirm the numbering independently: `KILSND EQU $13` "SOUND NUMBER TO TURN
OFF SOUNDS" and `LSOUND EQU $1F` "LAST SOUND TO BE TESTED" (`romc0.src:7-8`) match the
BGEND-at-`$13` / last-jump-table-entry-at-`$1F` decode exactly. `MULTI_SOURCE_CONFIRMED`.

### 2.4 The handler is deliberately re-entrant and never returns

`SOURCE_CONFIRMED`. `LDS #ENDRAM` + `CLI` at `VSNDRM1:909-911` means:

* the previous sound's return addresses and locals are discarded;
* a new command interrupts the current sound **between any two instructions**, including mid-DAC-write-loop;
* nothing ever executes `RTI`. When a sound finishes, control reaches `IRQ3` (`VSNDRM1:954-962`)
  which either jumps into a background routine or executes `BEQ *` — a branch-to-self idle loop.

Consequence for a reimplementation: **there is exactly one voice.** Any command hard-cuts the
previous sound with no release, no fade, and no crossfade. `MULTI_SOURCE_CONFIRMED` (source + MAME
IRQ wiring).

### 2.5 NMI is the diagnostic entry

`SOURCE_CONFIRMED` (`VSNDRM1:976-994`). NMI does an ADC-chain checksum of `$F800–$FFFF`, `WAI`s
(halting, which on this board extinguishes the activity LED) if it fails, then plays VARI vector 1
(`FOSHIT`) in a loop. Not reachable in normal play.

### 2.6 The speech hook is dead in Defender

`MULTI_SOURCE_CONFIRMED`. `VSNDRM1:930-933` tests whether `$EFFD` contains `$7E` (a `JMP` opcode)
and calls it if so — the hook for the Williams speech board used by other titles. Defender's sound
ROM occupies only `$F800–$FFFF`; `$EFFD` is unmapped. Defender has no speech.

---

## 3. Command dispatch

### 3.1 The dispatcher

`SOURCE_CONFIRMED` (`VSNDRM1:914-962`), annotated:

```
        LDAB ORGFLG
        BEQ  IRQ00          ; no organ sequence pending
        BPL  IRQ0           ; ORGFLG > 0  → ORGANN nibble/note feed
        JSR  ORGNT1         ; ORGFLG < 0  → this command IS a tune number (never returns)
IRQ0    DECA
        JSR  ORGNN1         ; ORGANN state machine (may never return)
IRQ00   CLRB
        CMPA #SP1SND        ; $0E
        BEQ  IRQ00A
        STAB SP1FLG         ; any command other than $0E resets the spinner counter
IRQ00A  CMPA #B2SND         ; $12
        BEQ  IRQ000
        STAB B2FLG          ; any command other than $12 resets the bonus latch
IRQ000  (speech hook)
IRQ1    TSTA
        BEQ  IRQ3           ; C = 0 → nothing
        DECA                ; A = C − 1
        CMPA #$C
        BHI  IRQ10
        JSR  GWLD           ; C = $01..$0D  → GWAVE sound, SVTAB index (C−1)
        JSR  GWAVE
        BRA  IRQ3
IRQ10   CMPA #$1B
        BHI  IRQ20
        SUBA #$D            ; C = $0E..$1C  → JMPTBL index (C − $0E)
        ASLA
        LDX  #JMPTBL
        BSR  ADDX
        LDX  0,X
        JSR  0,X
        BRA  IRQ3
IRQ20   SUBA #$1C           ; C = $1D..$1F  → VARI vector (C − $1D)
        JSR  VARILD
        JSR  VARI
IRQ3    LDAA BG1FLG
        ORAA BG2FLG
        BEQ  *              ; no background → idle here until the next IRQ
        CLRA
        STAA B2FLG
        LDAA BG1FLG
        BEQ  IRQXX
        JMP  BG1
IRQXX   JMP  BG2
```

Three disjoint command ranges:

| Range | Mechanism | Index |
| --- | --- | --- |
| `$01–$0D` | `GWLD` + `GWAVE` — table-driven wavetable engine | `SVTAB[C−1]` |
| `$0E–$1C` | `JMPTBL` — 15 hand-written routines | `JMPTBL[C−$0E]` |
| `$1D–$1F` | `VARILD` + `VARI` — variable-duty square engine | `VVECT[C−$1D]` |

### 3.2 `JMPTBL` — full dump

`SOURCE_CONFIRMED` (`VSNDRM1:998-1001`; addresses from my assembly of the file, cross-checked by the
exact 2 KB fit).

| Command | Table address | Vector value | Routine |
| --- | --- | --- | --- |
| `$0E` | `$FD58` | `$FB49` | `SP1` — spinner / lander suck |
| `$0F` | `$FD5A` | `$F913` | `BG1` — background drone (persistent) |
| `$10` | `$FD5C` | `$FB24` | `BG2INC` — advance background 2 |
| `$11` | `$FD5E` | `$F88C` | `LITE` — lightning |
| `$12` | `$FD60` | `$FB71` | `BON2` — laser-ball bonus #2 |
| `$13` | `$FD62` | `$FB1E` | `BGEND` — kill all backgrounds |
| `$14` | `$FD64` | `$F8CD` | `TURBO` — decaying white noise |
| `$15` | `$FD66` | `$F894` | `APPEAR` — rising noise |
| `$16` | `$FD68` | `$F91C` | `THRUST` — filtered noise (persistent) |
| `$17` | `$FD6A` | `$F923` | `CANNON` — distorted filtered noise |
| `$18` | `$FD6C` | `$F9A6` | `RADIO` — swept 16-point wavetable |
| `$19` | `$FD6E` | `$F9D4` | `HYPER` — PWM sweep |
| `$1A` | `$FD70` | `$F9F3` | `SCREAM` — 4-voice additive glissando |
| `$1B` | `$FD72` | `$FA44` | `ORGANT` — arm organ *tune* |
| `$1C` | `$FD74` | `$FA84` | `ORGANN` — arm organ *note* |

### 3.3 `SVTAB` — full dump

`SOURCE_CONFIRMED` (`VSNDRM1:1101-1116`). Base `$FEEC`, 7 bytes per entry. Byte layout per the
source comment at `VSNDRM1:1092-1099`:

```
byte 0: GECHO (high nibble) | GCCNT (low nibble)
byte 1: GECDEC (high nibble) | wave number (low nibble)
byte 2: pre-decay factor
byte 3: GDFINC   (signed frequency-offset increment)
byte 4: GDCNT    (frequency-offset repeat count; 0 means 256)
byte 5: frequency-pattern length
byte 6: frequency-pattern offset from GFRTAB
```

| Cmd | Label | Addr | Raw | echoes | cycles/step | echo-decay | wave | pre-decay | ΔF | ΔF count | pat len | pattern |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `$01` | `HBDV` | `$FEEC` | `81 24 00 00 00 16 31` | 8 | 1 | 2 | 4 `GSQ22` | 0 | 0 | – | 22 | `HBDSND` |
| `$02` | `STDV` | `$FEF3` | `12 05 1A FF 00 27 6D` | 1 | 2 | 0 | 5 `GS72` | `$1A` | −1 | 256 | 39 | `STDSND` |
| `$03` | `DP1V` | `$FEFA` | `11 05 11 01 0F 01 47` | 1 | 1 | 0 | 5 `GS72` | `$11` | +1 | 15 | 1 | `SWPAT` |
| `$04` | `XBV` | `$FF01` | `11 31 00 01 00 0D 1B` | 1 | 1 | 3 | 1 `GSSQ2` | 0 | +1 | 256 | 13 | `SPNSND` |
| `$05` | `BBSV` | `$FF08` | `F4 12 00 00 00 14 47` | 15 | 4 | 1 | 2 `GS1` | 0 | 0 | – | 20 | `BBSND` |
| `$06` | `HBEV` | `$FF0F` | `41 45 00 00 00 0F 5B` | 4 | 1 | 4 | 5 `GS72` | 0 | 0 | – | 15 | `HBESND` (+1 overrun) |
| `$07` | `PROTV` | `$FF16` | `21 35 11 FF 00 0D 1B` | 2 | 1 | 3 | 5 `GS72` | `$11` | −1 | 256 | 13 | `SPNSND` |
| `$08` | `SPNRV` | `$FF1D` | `15 00 00 FD 00 01 69` | 1 | 5 | 0 | 0 `GS2` | 0 | −3 | 256 | 1 | `SPNR` |
| `$09` | `CLDWNV` | `$FF24` | `31 11 00 01 00 03 6A` | 3 | 1 | 1 | 1 `GSSQ2` | 0 | +1 | 256 | 3 | `COOLDN` |
| `$0A` | `SV3` | `$FF2B` | `01 15 01 01 01 01 47` | **256** | 1 | 1 | 5 `GS72` | 1 | +1 | 1 | 1 | `BBSND` |
| `$0B` | `ED10` | `$FF32` | `F6 53 03 00 02 06 94` | 15 | 6 | 5 | 3 `GS12` | 3 | 0 | 2 | 6 | `ED10FP` |
| `$0C` | `ED12` | `$FF39` | `6A 10 02 00 02 06 9A` | 6 | 10 | 1 | 0 `GS2` | 2 | 0 | 2 | 6 | `ED13FP` |
| `$0D` | `ED17` | `$FF40` | `1F 12 00 FF 10 04 69` | 1 | 15 | 1 | 2 `GS1` | 0 | −1 | 16 | 4 | `SPNR` (+3 overrun) |
| — | `BONV` | `$FF47` | `31 11 00 FF 00 0D 00` | 3 | 1 | 1 | 1 `GSSQ2` | 0 | −1 | 256 | 13 | `BONSND` |
| — | `TRBV` | `$FF4E` | `12 06 00 FF 01 09 28` | 1 | 2 | 0 | 6 `GS1.7` | 0 | −1 | 1 | 9 | `TRBPAT` |

`BONV` (index 13) and `TRBV` (index 14) are **not** reachable from the command decoder, whose GWAVE
range stops at index 12. They are loaded only by `BON2` (`LDAA #(BONV-SVTAB)/7`, `VSNDRM1:719`) and
`BG2` (`LDAA #(TRBV-SVTAB)/7`, `VSNDRM1:678`). `SOURCE_CONFIRMED`.

Note `GECHO = 0` in `SV3` is not "no echoes": `GECNT` is decremented before it is tested
(`GEND40 DEC GECNT / BNE GWT4`, `VSNDRM1:822-823`), so 0 means **256 passes**. Likewise
`GDCNT = 0` means 256 frequency-offset steps (`GEND50`, `VSNDRM1:826-829`). `SOURCE_CONFIRMED`.

### 3.4 `VVECT` — full dump

`SOURCE_CONFIRMED` (`VSNDRM1:1005-1009`). Base `$FD76`, 9 bytes per entry, transferred verbatim
into RAM at `$13` by `VARILD` (`VSNDRM1:194-204`).

| Cmd | Label | Addr | `LOPER` | `HIPER` | `LODT` | `HIDT` | `HIEN` | `SWPDT` | `LOMOD` | `VAMP` |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `$1D` | `SAW` | `$FD76` | `$40` (64) | `$01` | 0 | `$10` (+16) | `$E1` (225) | `$0080` (128) | `$FF` (−1) | `$FF` |
| `$1E` | `FOSHIT` | `$FD7F` | `$28` (40) | `$01` | 0 | `$08` (+8) | `$81` (129) | `$0200` (512) | `$FF` (−1) | `$FF` |
| `$1F` | `QUASAR` | `$FD88` | `$28` (40) | `$81` (129) | 0 | `$FC` (−4) | `$01` | `$0200` (512) | `$FC` (−4) | `$FF` |
| — | `CABSHK` | `$FD91` | `$FF` (255) | `$01` | 0 | `$18` (+24) | `$41` (65) | `$0480` (1152) | `$00` | `$FF` |

`CABSHK` ("cabinet shake", index 3) is reachable only through `SP1`, which overwrites `LOPER`.

### 3.5 Other table addresses

`SOURCE_CONFIRMED` (my assembly):

```
GWVTAB $FE4D   GS2 $FE4D  GSSQ2 $FE56  GS1 $FE5F  GS12 $FE70
               GSQ22 $FE81  GS72 $FE92  GS1.7 $FEDB
GFRTAB $FF55   BONSND $FF55  HBTSND $FF62  SPNSND $FF70  TRBPAT $FF7D
               HBDSND $FF86  SWPAT=BBSND $FF9C  HBESND $FFB0  SPNR $FFBE
               COOLDN $FFBF  STDSND $FFC2  ED10FP $FFE9  ED13FP $FFEF
RADSND $FD9A   ORGTAB $FDAA  NOTTAB $FE41
```

---

## 4. RAM map

`SOURCE_CONFIRMED` (`VSNDRM1:48-136`; the mwenge copy independently pins `LOCRAM EQU $0013`,
`vsndrm1:66`). `$13` upward is a union — each engine overlays its own parameter block.

```
$00-$03  TLKGL    speech globals (unused in Defender)
$04      BG1FLG   background 1 active
$05      BG2FLG   background 2 state (0 = off, 1..29 = pitch step)
$06      SP1FLG   spinner step counter (1..31)
$07      B2FLG    bonus-2 latch
$08      ORGFLG   organ state machine ($FF = tune armed, 3/2/1 = note armed)
$09      HI       random shift register, high byte  (seeded $3C at reset)
$0A      LO       random shift register, low byte   (NEVER initialised)
$0B-$0C  TEMPX    $0D-$0E XPLAY   $0F-$10 XPTR   $11 TEMPA   $12 TEMPB
$13...   LOCRAM — per-engine overlay:

 GWAVE : $13 GECHO  $14 GCCNT  $15 GECDEC $16 GDFINC $17 GDCNT
         $18 GWFRM  $1A PRDECA $1B GWFRQ  $1D FRQEND $1F WVEND
         $21 GPER   $22 GECNT  $23 FOFSET $24..$6B GWTAB (72-byte wave buffer)
 VARI  : $13 LOPER  $14 HIPER  $15 LODT   $16 HIDT   $17 HIEN
         $18 SWPDT  $1A LOMOD  $1B VAMP   $1C LOCNT  $1D HICNT
 NOISE : $13 DECAY  $14 NAMP   $15 CYCNT  $16 NFRQ1  $18 NFFLG
         $19 LFREQ  $1A DFREQ
 FNOISE: $13 FMAX   $14 FHI    $15 FLO    $16 SAMPC  $18 FDFLG  $19 DSFLG
 SCREAM: $13..$1A STABLE (4 × {FREQ, TIMER}),  SRMEND = $1B
 ORGAN : $13 DUR    $15 OSCIL  $16..$51 RDELAY (60-byte self-modifying delay)
```

`GWTAB` ends at `$6B`, leaving `$6C–$7F` (20 bytes) for the stack — enough for the deepest
`JSR` chain in the program. `INFERRED`.

**Latent out-of-bounds write.** `SCREAM`'s "start the next echo" store is `STAB FREQ+2,X`
(`VSNDRM1:506`). When `X` addresses the last voice (`$19`) this writes `$41` to `$1B`, one byte past
`SRMEND`. `$1B` is `GWFRQ`'s high byte in the GWAVE overlay, which `SCREAM` does not use, so the
write is harmless — but a reimplementation that models RAM must not treat `STABLE` as a clean
4-element array. `SOURCE_CONFIRMED`.

---

## 5. The timing model

This is the section that determines whether a reimplementation is in tune.

**5.1 There is no fixed sample rate.** `SOURCE_CONFIRMED`. Every routine writes the DAC from inside
a loop whose length it controls. To reproduce pitch you must count 6800 cycles.

**5.2 Cycle counts used.** MC6800/6808 timings: inherent register ops 2; branches 4 (taken *or*
not); `DEX`/`INX` 4; `NOP` 2; immediate 8-bit 2, immediate 16-bit 3; direct load 3 / store 4; direct
16-bit load 4 / store 5 / `CPX` 4; extended load 4 / store 5; indexed load 5 / store 6;
read-modify-write (`INC`/`DEC`/`COM`/`CLR`/`ROR`/`LSR`/`TST`) extended 6, indexed 7 — **these
instructions have no direct-page mode on the 6800**, so `COM SOUND`, `DEC GDCNT` and `ROR HI` all
cost 6; `JMP` extended 3; `JSR` extended 9, indexed 8; `BSR` 8; `RTS` 5.

**5.3 Independent validation of the cycle model.** `MULTI_SOURCE_CONFIRMED`. Two checks:

1. **The 36-cycle pad.** `GWAVE`'s wave-wrap path contains four `INX`/`DEX` pairs plus two `NOP`s,
   commented `BRA GOUT   SYNC 36` (`VSNDRM1:809-819`). 4 × (4+4) + 2 × 2 = **36**. The comment is
   the author stating his own cycle count, and it matches mine exactly.
2. **The organ scale.** The organ's per-sample loop costs 69 cycles plus a self-built delay of `d`
   cycles plus a 3-cycle `JMP` back, i.e. **72 + d**. With `d` taken from `NOTTAB`
   (`VSNDRM1:1064-1066`) the resulting periods are 76, 80, 85, 90, 95, 101, 107, 113, 120, 127, 135,
   143 — a twelve-step equal-tempered scale, since 143/76 = 1.882 ≈ 2^(11/12) and the next octave
   would be 152 = 2 × 76. A twelve-tone scale falls out of the constant "72" only if the constant is
   right. Independently, the note-duration constants `TAF..TG` (`VSNDRM1:22-33`) satisfy
   `T × (72+d) ≈ 2 478 000 ± 0.4 %` across all twelve notes, i.e. they were chosen so that a given
   note value lasts the same wall-clock time in every key. Both facts pin the "72".

**5.4 Per-engine sample periods.** All figures below are derived by summing the cycle counts in
§5.2 around the DAC write. `SOURCE_CONFIRMED` for the instruction sequence, `INFERRED` for the
arithmetic (but see §5.3 — the arithmetic is validated twice).

| Engine | Cycles per DAC write | Sample rate at E = 894 886 Hz |
| --- | --- | --- |
| `GWAVE` (`GOUTLP`) | `25 + 6·GPER` (+49 once per wave pass) | **573 Hz … 28 867 Hz** |
| `VARI` (`V1`/`V2`) | 14 per half-cycle *step*; DAC only toggles at edges | edge-driven |
| `NOISE`/`TURBO` (`NOISE1`) | `≈48.5 + 8·NFRQ1` | 428 Hz … 15.8 kHz |
| `LITE`/`APPEAR` (`LITE1`) | `≈42 + 6·LFREQ` | 569 Hz … 18.6 kHz |
| `FNOISE` (`FNOIS3`/`FNOIS4`) | 30 per slew step, +64…67 per new target | 29.8 kHz slew clock |
| `SCREAM` (`SCREM2`) | ≈196 | 4 566 Hz |
| `RADIO` (`RADIO1`) | 60 (deliberately equalised on both branches) | 14 915 Hz |
| `HYPER` (`HYPER2`) | 123 per step, 129 on the match step | edge-driven, 56.75 Hz carrier |
| `ORGAN` (`ORGAN1`) | `72 + d`, `d` ∈ {4…71} | 11 775 Hz … 6 259 Hz |

The `±1.5`-cycle fractions in the noise rows come from conditional branches taken half the time
(`BCC` skipping a `COM SOUND` or `LDAA NAMP`); I have used the expectation.

> **Correction, 2026-08-09.** The `GWAVE` row previously read "1 148 Hz … 35.8 kHz". Neither endpoint
> reproduces from the stated formula. Evaluating `E / (25 + 6·GPER)` at `E = 894 886.25 Hz`:
> `GPER = 0` **wraps to 256** (the loop is `DECA / BNE`, so a zero period runs the full 256
> iterations) → `25 + 1536 = 1561` cycles → **573 Hz**; `GPER = 1` → `31` cycles → **28 867 Hz**.
> The old `35.8 kHz` is `E / 25`, i.e. the delay term dropped entirely, which `GPER` cannot do.
> Every other row in this table *was* checked and reproduces exactly — `NOISE`/`TURBO`
> `48.5 + 8·NFRQ1` → 428 Hz … 15 839 Hz; `LITE`/`APPEAR` `42 + 6·LFREQ` → 569 Hz … 18 643 Hz;
> `FNOISE` 30 cycles → 29 829 Hz; `SCREAM` 196 → 4 566 Hz; `RADIO` 60 → 14 915 Hz; `ORGAN`
> `72 + d` → 11 775 … 6 258 Hz. This was an isolated slip, not a modeling error.
>
> **Superseded in part, 2026-08-09 (Phase 2 audio).** The `GWAVE` tail term is **45** cycles, not
> 49, on the pass that consumes the last `GCCNT` repeat of a pattern entry; 49 applies only to the
> intermediate repeats, which take the 36-cycle pad *plus* `BRA GOUT`. `GPLAY` itself is 36 cycles
> and falls straight into `GOUT`. Consequences: the maximum pass rate is
> `894 886.25 / (8·31 + 45)` = **3 054.2 Hz**, not 3 013 Hz; `$01`'s first pass is 1 654.1 Hz
> (audible tone 3 308.2 Hz), not 1 642 / 3 284 Hz; `$0D`'s last pass is 1 654.1 Hz; `$09`'s first
> pass is 883.4 Hz. The per-sample rate range (573 Hz … 28 867 Hz) is unchanged. Two further
> corrections from the same pass: `$19 HYPER`'s duty runs **down** (high-time = (128 − `TEMPA`)/128,
> so 100 % → 0.78 %), and the `$1D`/`$1E`/`$1F` frequency ranges in §6.2 and in the per-command
> tables conflate the start of sweep 1 with the end of sweep N — see `SOUND_EVENT_MATRIX.md` §7
> C-3 for the per-sweep figures. Every duration in this document was re-verified by cycle-accurate
> simulation against `defend.snd` and reproduces to within rounding.
>
> **Do not confuse the per-sample DAC write rate with the pass (fundamental) rate.** The pass rate is
> `E / (L·(25 + 6·GPER) + 49)` for a waveform of `L` bytes. Its maximum, at the shortest waveform
> `L = 8` and `GPER = 1`, is `894 886.25 / 297` = **3 013 Hz** — the figure this document already
> uses in its own per-command table (e.g. `$04 XBV`, `$08 SPNRV`). `GWAVE` cannot produce a tone
> above ~3 kHz.

---

## 6. The synthesis engines

Defender's sound program contains **nine** distinct synthesis methods. Every one of the 31 commands
routes to one of them.

### 6.1 `GWAVE` — wavetable with pattern-driven period, echo, and decay (`$FBE7`)

The workhorse: 13 of the 31 commands, plus background 2 and the bonus.

**Structure.** `GWLD` (`$FB81`) copies one of seven ROM waveforms into the 72-byte RAM buffer
`GWTAB`, applies a "pre-decay", and points at a *frequency pattern* — a list of 8-bit period bytes.
`GWAVE` (`$FBE7`) then plays the waveform once per period byte (or `GCCNT` times), walking the
pattern, then decays the waveform and repeats for `GECHO` echoes, then optionally shifts the whole
pattern by a growing offset and starts over.

**Inner loop** (`VSNDRM1:798-819`):

```
GOUT    LDX  #GWTAB
GOUTLP  LDAA GPER
GPRLP   DECA
        BNE  GPRLP           ; 6·GPER cycles of dead time
        LDAA ,X
        STAA SOUND           ; one DAC sample
GPR1    INX
        CPX  WVEND
        BNE  GOUTLP
        DECB                 ; GCCNT repeats
        BEQ  GPLAY
        INX/DEX ×4 + NOP ×2  ; SYNC 36
        BRA  GOUT
```

**Pitch.** One pass over an `L`-sample waveform takes

```
T_pass = L · (25 + 6·GPER) + 49   cycles          (GPER = 0 means 256)
f_pass = 894 886 / T_pass          Hz
```

For waveforms holding one cycle, `f_pass` is the fundamental. `GSQ22` holds **two** square-wave
cycles per table, so its fundamental is `2 · f_pass`. `SOURCE_CONFIRMED` for the loop,
`INFERRED` for the arithmetic.

**Waveform tables** (`VSNDRM1:1071-1089`, first byte is the length). `SOURCE_CONFIRMED`:

```
0 GS2   L=8   127,217,255,217,127,36,0,36                       one sine cycle, 8 pts
1 GSSQ2 L=8   0,64,128,0,255,0,128,64                           harsh non-sinusoid, 1 cycle
2 GS1   L=16  127,176,217,245,255,245,217,176,
              127,78,36,9,0,9,36,78                             one sine cycle, 16 pts
3 GS12  L=16  127,197,236,231,191,141,109,106,
              127,148,146,113,64,23,18,57                       sine + 2nd harmonic
4 GSQ22 L=16  255,255,255,255,0,0,0,0,
              255,255,255,255,0,0,0,0                           square, TWO cycles per table
5 GS72  L=72  138,149,160,171,181,191,200,209,218,225,232,238,
              243,247,251,253,254,255,254,253,251,247,243,238,
              232,225,218,209,200,191,181,171,160,149,138,127,
              117,106,95,84,74,64,55,46,37,30,23,17,12,8,4,2,1,0,
              1,2,4,8,12,17,23,30,37,46,55,64,74,84,95,106,117,127
                                                                one sine cycle, 72 pts
6 GS1.7 L=16  89,123,152,172,179,172,152,123,
              89,55,25,6,0,6,25,55                              sine × ≈0.70
```

**Frequency-pattern tables** (`VSNDRM1:1120-1153`). `SOURCE_CONFIRMED`:

```
BONSND  $A0,$98,$90,$88,$80,$78,$70,$68,$60,$58,$50,$44,$40      13
HBTSND  1,1,2,2,4,4,8,8,$10,$10,$30,$60,$C0,$E0                  14  ← never referenced
SPNSND  1,1,2,2,3,4,5,6,7,8,9,$A,$C                              13
TRBPAT  $80,$7C,$78,$74,$70,$74,$78,$7C,$80                       9
HBDSND  1,1,2,2,4,4,8,8,$10,$20,$28,$30,$38,$40,$48,$50,
        $60,$70,$80,$A0,$B0,$C0                                  22
SWPAT = BBSND
BBSND   8,64 repeated 10 times                                   20
HBESND  1,2,4,8,9,$A,$B,$C,$E,$F,$10,$12,$14,$16                 14
SPNR    $40                                                       1
COOLDN  $10,8,1                                                   3
STDSND  1,1,1,1,2,2,3,3,4,4,5,6,8,$A,$C,$10,$14,$18,$20,$30,
        $40,$50,$40,$30,$20,$10,$C,$A,8,7,6,5,4,3,2,2,1,1,1      39
ED10FP  7,8,9,$A,$C,8                                             6
ED13FP  $17,$18,$19,$1A,$1B,$1C,0,0,0                           6 + 3 pad
```

**Two table-length mismatches are real and audible.** `SOURCE_CONFIRMED` by address arithmetic:

* `HBEV` declares 15 pattern entries but `HBESND` is 14 bytes long. The 15th byte read is `SPNR`'s
  `$40` at `$FFBE`. The lander-hit sound therefore ends on a period-64 (30.3 Hz) growl that is not
  in its own table.
* `ED17` declares 4 entries starting at `SPNR` (`$FFBE`), which is 1 byte long. Its pattern is
  actually `$40, $10, $08, $01` — `SPNR` followed by all of `COOLDN`.
* `HBTSND` ("HUNDRED POINT SOUND") is referenced by nothing. Dead data.

**Envelope: `WVDECA`** (`VSNDRM1:879-905`). `SOURCE_CONFIRMED`:

```
for i in 0..L-1:  RAM[i] = (RAM[i] − factor · (ROM[i] >> 4)) mod 256
```

Decay is proportional to each sample's *original* magnitude, one sixteenth per unit, so 16 units
takes the waveform to a flat line — **but the subtraction is unsigned and wraps**. Applying more
than 16 units total does not silence the sound, it folds it over into a progressively more
distorted waveform. `SV3` (command `$0A`, "START 1") runs 256 echoes with `GECDEC = 1`, so it spends
almost its entire 1.5-second life in the wrapped-around regime. This is not a bug being exploited by
accident — it is the reason that sound has its character. `INFERRED` for the intent, `SOURCE_CONFIRMED`
for the mechanism.

Because the decay pulls samples toward `$00` rather than toward mid-scale, a decaying `GWAVE` sound
also drifts its DC level downward. On the real board this is removed by AC coupling.

**Echo loop and frequency modulation** (`VSNDRM1:785-863`). After the pattern is exhausted:

1. `WVDECA(GECDEC)`; decrement `GECNT`; if non-zero, replay the pattern from the top.
2. If `GDFINC = 0`, stop.
3. Decrement `GDCNT`; if it reaches 0, stop.
4. `FOFSET += GDFINC`. Every period byte is now played as `pattern[i] + FOFSET`.
5. **Window narrowing** (`GW0`…`GW3`): rescan the pattern and keep only the longest leading run of
   entries for which `pattern[i] + FOFSET` does not wrap 8 bits (for `GDFINC ≥ 0`, "wrap" = carry
   out; for `GDFINC < 0`, "wrap" = result 0 or no carry). `GWFRQ` and `FRQEND` are moved inward. If
   no entry survives, the sound ends.
6. If `GECDEC ≠ 0`, re-copy the waveform from ROM and re-apply the pre-decay — i.e. the amplitude
   envelope **restarts** on every frequency-offset step.
7. Jump back into `GWAVE`.

This is how the sweeping sounds work: a fixed pattern shape slid up or down in period space until it
runs off the end of 8-bit range.

**Derived pitch and duration for every GWAVE sound.** `INFERRED` (from a cycle-accurate simulation
of the state machine above; the per-pass formula is from §6.1 and the state machine from
`VSNDRM1:785-863`):

| Cmd | Vector | Passes | First → last pass rate | Total | Notes |
| --- | --- | --- | --- | --- | --- |
| `$01` | `HBDV` | 176 | 1 642 → 47.4 Hz | 1.21 s | square wave ⇒ audible tone is **2×** these figures (3 284 → 94.8 Hz); 8 identical descending sweeps, amplitude 255→45 |
| `$02` | `STDV` | 569 | 392 → 392 Hz | 14.0 s | pattern is a rise-and-fall; window shrinks from both ends |
| `$03` | `DP1V` | 15 | 168.7 → 78.8 Hz | 0.14 s | single-entry pattern swept down 15 steps |
| `$04` | `XBV` | 3 258 | 3 013 → 71.7 Hz | 23.7 s | orphan command |
| `$05` | `BBSV` | 300 | alternates 735 / 136 Hz | 5.24 s | "Big Ben" — 15 echoes of a 20-note two-tone alternation |
| `$06` | `HBEV` | 60 | 392 → 30.3 Hz | 0.56 s | 4 echoes, deep decay (`GECDEC` = 4) |
| `$07` | `PROTV` | 140 | 392 → 392 Hz | 0.58 s | 2 echoes then a rising offset sweep |
| `$08` | `SPNRV` | 22 | 269.5 → 3 013 Hz | 0.22 s | `GDFINC = −3`: single period byte driven up in threes |
| `$09` | `CLDWNV` | 2 229 | 880 → 71.7 Hz | 16.4 s | "COOL DOWNER" — very long descent |
| `$0A` | `SV3` | 256 | 168.7 Hz constant | 1.52 s | pitch fixed; the whole event is the waveform corrupting |
| `$0B` | `ED10` | 90 | 798 → 735 Hz | 0.79 s | 15 echoes × 6 notes × 6 repeats |
| `$0C` | `ED12` | 36 | 661 → 562 Hz | 0.59 s | 6 echoes × 6 notes × 10 repeats |
| `$0D` | `ED17` | 41 | 136 → 1 642 Hz | 2.04 s | orphan command |
| — | `BONV` | 4 356 | 113 → 3 013 Hz | 15.3 s free-run | in-game it is stepped one offset at a time by repeated `$12` commands |
| — | `TRBV` | 9 | 70.3 → 79.9 Hz | 0.24 s per pass | background-2 turbine; `BG2` re-arms it forever |

### 6.2 `VARI` — variable-duty square with dual sweep (`$F83F`)

`SOURCE_CONFIRMED` (`VSNDRM1:208-246`). Commands `$1D`, `$1E`, `$1F`, plus `SP1`.

The DAC alternates hard between `VAMP` (`$FF` in every vector) and its complement (`$00`). Each
half-cycle is a `DEX/BEQ/DECA/BNE` loop costing **14 cycles per step**:

```
T_period = 22 + 14·(LOCNT + HICNT)   cycles
f        = 894 886 / T_period        Hz
duty     = HICNT / (LOCNT + HICNT)
```

A separate 16-bit counter `SWPDT` counts **inner steps** (not periods). When it expires:
`LOCNT += LODT`, `HICNT += HIDT`, the output phase is force-normalized
(`LDAA SOUND / BMI / COMA / STAA SOUND`), and the sweep continues until `HICNT == HIEN`. Then
`LOPER += LOMOD` and the whole sweep restarts, until `LOPER` wraps to zero. `LOMOD = 0` means one
sweep and return.

Because `LODT = 0` in all four vectors, the low half-period is constant within a sweep and only the
high half moves — this is a *pulse-width* sweep that also drops the pitch, not a pure glide.

Derived behavior (`INFERRED`, arithmetic from the loop above):

| Cmd | Vector | Sweep steps | Step time | One sweep | Sweeps | Total | Pitch |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `$1D` | `SAW` | 14 | 2.05 ms | 28.8 ms | 64 | 1.84 s | 960 → 302 Hz each sweep, rising sweep-to-sweep |
| `$1E` | `FOSHIT` | 16 | 8.06 ms | 129 ms | 40 | 5.16 s | 1 501 → 517 Hz each sweep, rising sweep-to-sweep |
| `$1F` | `QUASAR` | 32 | 8.06 ms | 258 ms | 10 | 2.58 s | 375 → 6 047 Hz — **rising** (`HIDT` negative) |
| — | `CABSHK` | 24 | 18.07 ms | 434 ms | 1 | 0.43 s | depends on `LOPER`, see `SP1` |

`HIEN` is tested for exact equality (`CMPA HIEN / BNE V0`, `VSNDRM1:239-240`), and for `CABSHK`
`HIPER = 1`, `HIDT = 24`, `HIEN = 65` never coincide in the first pass — the counter wraps modulo
256 and hits 65 on step 24. `SOURCE_CONFIRMED`; an implementation that assumes the sweep terminates
after (65−1)/24 steps will end the sound eight times too early.

### 6.3 `SP1` — the spinner (`$FB49`)

`SOURCE_CONFIRMED` (`VSNDRM1:691-712`). Loads `CABSHK`, then computes a new `LOPER` from a counter
that advances by one on every consecutive `$0E` command and resets on any other command
(`IRQ00`, `VSNDRM1:920-923`):

```
SP1FLG: 1,2,…,31 then back to 1
A = 32 − SP1FLG
B = 0;  while A > 20: B += 14, A -= 1;  repeat { B += 5; A -= 1 } until A == 0
LOPER = B
```

giving `LOPER` = 254, 240, 226, …, 114, 100, 95, 90, …, 10, 5. Then `SP1LP JSR VARI / BRA SP1LP` —
it loops the whole `CABSHK` sweep forever until interrupted.

Derived pitch schedule (`INFERRED`, base frequency at the start of each sweep):

| `SP1FLG` | 1 | 5 | 10 | 15 | 20 | 24 | 27 | 29 | 31 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `LOPER` | 254 | 198 | 128 | 85 | 60 | 40 | 25 | 15 | 5 |
| f at `HICNT`=1 | 249 Hz | 319 | 490 | 730 | 1 022 | 1 501 | 2 318 | 3 638 | 8 442 Hz |

The game drives this with `LSKSND = $C8,$0A,$01,$0E,0` — ten `$0E` commands 16 ms apart
(`defa7.src:678`). Each command restarts the sweep at a higher pitch, so what you hear is a
ten-step rising staircase of short buzzes, ≈160 ms total: the lander hauling an astronaut upward.
`MULTI_SOURCE_CONFIRMED`.

### 6.4 `LITE` / `APPEAR` — random-toggle square (`$F88C` / `$F894`)

`SOURCE_CONFIRMED` (`VSNDRM1:250-289`). Full-scale output that inverts whenever the shift register
emits a 1:

```
LITE1  <advance shift register, carry = output bit>
       BCC LITE2
       COM SOUND
LITE2  LDAA LFREQ ; DECA/BNE loop   ← the sample clock
       DECB / BNE LITE1             ← CYCNT samples per LFREQ value
       LFREQ += DFREQ ; loop until LFREQ wraps to 0
```

| Cmd | Routine | `LFREQ` | `DFREQ` | `CYCNT` | Sample rate | Duration |
| --- | --- | --- | --- | --- | --- | --- |
| `$11` | `LITE` | 1 → 255 | +1 | 3 | 18 643 → 569 Hz | 692 ms |
| `$15` | `APPEAR` | 192 → 2 | −2 | 16 | 749 → 16 572 Hz | 1 071 ms |

`INFERRED` (arithmetic). `LITE` is a bright crackle collapsing into a low rumble; `APPEAR` is its
mirror image, a low rumble accelerating into a hiss. Both are constant amplitude — the *only*
envelope is the spectral one.

### 6.5 `TURBO` / `NOISE` — amplitude-decaying white noise (`$F8CD` / `$F8DC`)

`SOURCE_CONFIRMED` (`VSNDRM1:293-334`). Output is `0` or `NAMP` on each shift-register bit, so the
amplitude envelope is written directly into the DAC code:

```
32 samples per block (CYCNT), sample period ≈ 48.5 + 8·NFRQ1
after each block:  NAMP -= DECAY;  if NFFLG: NFRQ1 += 1
stop when NAMP hits 0
```

`TURBO` sets `NFRQ1 = 1`, `NAMP = $FF`, `DECAY = 1`, `NFFLG = $20` — so 255 blocks, amplitude
falling linearly 255 → 1 while the sample rate falls 15 839 → 428 Hz. Total **9.78 s** if never
interrupted. `INFERRED` (arithmetic). In play it is always cut short (see §8); the audible event is
the first ~150 ms.

`NOISE` is written as a general entry point (`X` = initial period, `B` = initial amplitude,
`A` = decay rate) but `TURBO` is its only caller. `SOURCE_CONFIRMED`.

### 6.6 `FNOISE` — slew-rate-limited noise (`$F930`)

`SOURCE_CONFIRMED` (`VSNDRM1:364-426`). This is Defender's signature texture and the engine behind
the background drone, thrust and every explosion.

The DAC level `A` **slews** toward a random 8-bit target at a rate of `FHI + FLO/256` per step, and
a new target is drawn each time it arrives:

```
FNOIS1: advance shift register; target = LO
        FHI = FMAX          (or FMAX AND HI if DSFLG — "DISTORT FREQUENCY")
        slew A up or down toward target, writing SOUND every step
FNOIS5: A = LO; write SOUND; new target
every SAMPC slew steps (FNOIS6), if FDFLG:
        {FMAX:FLO} -= {FMAX:FLO} >> 3        ; a 7/8 geometric decay
        stop when FMAX = 0 and FLO = 7
```

Each slew step costs **30 cycles** = 33.5 µs, so the slew clock is a constant 29.83 kHz and the
*spectrum* is set entirely by `FHI`: bigger `FHI` = faster edges = brighter noise. This is a
low-pass filter implemented as a slew limiter. `SOURCE_CONFIRMED` for the loop, `INFERRED` for the
33.5 µs.

The 7/8 decay chain from `$FF00` converges to exactly 7 in **72 steps** (`INFERRED`, by iteration),
which is why the terminating test is `CMPB #7`.

| Cmd | Routine | `FMAX` | `DSFLG` | `FDFLG` | `SAMPC` | Character | Duration |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `$0F` | `BG1` | 1 | 0 | 0 | (`$F913`, irrelevant) | mean segment 64 steps ≈ 2.15 ms → ≈230 Hz rumble | **infinite** |
| `$16` | `THRUST` | 3 | 0 | 0 | (`$F91C`, irrelevant) | mean segment 21.7 steps ≈ 0.73 ms → ≈660 Hz roar | **infinite** |
| `$17` | `CANNON` | `$FF` | 1 | 1 | 1000 | `FHI` randomised per segment; 72 decay steps | 2.54 s |

`BG1` and `THRUST` reach `FNOISE` with `X` holding **their own routine address**, because the
dispatcher's `LDX 0,X / JSR 0,X` leaves it there (`VSNDRM1:948-949`). `SAMPC` is therefore `$F913`
and `$F91C` respectively — an accident. It does not matter: with `FDFLG = 0`, `FNOIS6` branches
straight back to `FNOIS1` without reloading `X`, so the counter free-runs and the routine never
terminates. `SOURCE_CONFIRMED`.

`CANNON`'s `DSFLG = 1` ANDs `FMAX` with the shift-register high byte, so the slew rate is re-drawn
at random for every segment. That is what makes the explosion sound granular rather than like
filtered hiss. `SOURCE_CONFIRMED`.

### 6.7 The four one-off engines

**`SCREAM` (`$F9F3`)** — `SOURCE_CONFIRMED` (`VSNDRM1:475-515`). Four-voice additive synthesis with
8-bit phase accumulators. Each voice contributes its amplitude when its accumulator's MSB is set;
amplitudes are `$80, $40, $20, $10` (halved per voice by `LSR TEMPA`). Loop cost ≈196 cycles ⇒
**4 566 Hz sample rate**, so voice frequency = `FREQ/256 × 4 566 Hz`.

Voice 1 starts at `FREQ = $40` (1 141 Hz). Every 256 samples (56.1 ms) every non-zero `FREQ` is
decremented by 1; when a voice passes `$37` (981 Hz) the *next* voice is started at `$41`. Voices
die at `FREQ = 0`. Total **95 blocks ≈ 5.33 s** (`INFERRED`, by simulation). The result is four
detuned square waves glissando-ing from ~1.1 kHz down to sub-audio at staggered start times — the
falling astronaut's scream, with heavy beating between the offset voices.

**`RADIO` (`$F9A6`)** — `SOURCE_CONFIRMED` (`VSNDRM1:430-452`). A 16-point wavetable driven by a
16-bit phase accumulator whose *increment* rises by 1 on every accumulator overflow. Both branches
of the carry test are padded to equal length (`BRA *+2  EQUALIZE TIME`), so the sample period is a
constant **60 cycles = 14 915 Hz**. Table index is `accumulator_high & $F`, so one waveform cycle is
4 096 accumulator counts:

```
f = 14 915 · increment / 4096      increment starts at 100
```

Start 364 Hz; passes 500 Hz at 1.4 s, 1 kHz at 4.4 s, 2 kHz at 7.5 s, 4 kHz at 10.6 s
(`INFERRED`, by integration of `Σ 65536/inc`). The routine only exits when the increment wraps at
65 536 — nominally 28.5 s, so in practice it is always interrupted. Waveform
(`VSNDRM1:1013-1014`): `$8C,$5B,$B6,$40,$BF,$49,$A4,$73,$73,$A4,$49,$BF,$40,$B6,$5B,$8C` — a
symmetric, extremely jagged shape, peak-to-peak only `$40..$BF` (half scale).

**`HYPER` (`$F9D4`)** — `SOURCE_CONFIRMED` (`VSNDRM1:456-471`). Pulse-width modulation. Each carrier
cycle is 128 time steps of 123 cycles (129 on the step where the phase counter equals `TEMPA`), so
the carrier is a constant **56.75 Hz**; the DAC toggles once at position `TEMPA` and once at the end
of the cycle, giving duty = `TEMPA/128`. `TEMPA` increments each cycle, so the duty ramps 0 → 100 %
over 128 carrier cycles = **2.26 s** (`INFERRED`). A 56.75 Hz square with sweeping duty is a buzz
whose harmonic notches slide — this is the coin sound.

**`ORGAN` (`$FADB`)** — see §6.8.

### 6.8 The organ

`SOURCE_CONFIRMED` (`VSNDRM1:519-639`, `1016-1066`). This is a genuine additive octave-stack organ,
and it is the only part of Defender's audio that is *music*.

**Voicing.** A free-running 8-bit counter `TEMPB` is incremented once per sample; bit *k* of that
counter is a square wave at `rate / 2^(k+1)`. `OSCIL` is a bitmask selecting which bits participate,
and the DAC receives `16 × popcount(TEMPB AND OSCIL)`:

```
ORGAN1  CLRA / LDAB TEMPB / INCB / STAB TEMPB
        ANDB OSCIL
        LSRB, ADCA #0   ×7          ; sum bits 0..6
        ABA                          ; plus bit 7
        ASLA ×4                      ; ×16
        STAA SOUND
        DEX / BEQ ORGAN2 / JMP RDELAY
```

The **highest** set bit of the mask sets the perceived fundamental; the remaining bits add octaves
above it. Masks used in the tune table are `$FE`, `$F8` (fundamental = bit 7), `$7F`, `$7C`
(bit 6), `$3E` (bit 5), `$1F` (bit 4), containing 5 or 7 partials. Peak DAC swing is
`16 × popcount` = 80 (5 partials) or 112 (7 partials), so the organ is noticeably quieter than the
effects, and 7-partial notes are ~3 dB louder than 5-partial ones. `SOURCE_CONFIRMED` for the code,
`INFERRED` for the loudness reading.

**Tuning.** `ORGANL` (`VSNDRM1:584-604`) *writes a delay loop into RAM*: it fills `RDELAY` with
`NOP`s (2 cycles each), optionally one `CMPA <direct>` (3 cycles) to make an odd total, then a
`JMP ORGAN1`. The delay is therefore exactly `d` cycles where `d` is the note byte. Combined with
the 69-cycle body and the 3-cycle `JMP RDELAY`:

```
sample period = 72 + d cycles          f = 894 886 / ((72+d) · 2^(k+1))
```

`NOTTAB` (`VSNDRM1:1064-1066`) and the resulting frequencies (`INFERRED` arithmetic, validated by
§5.3):

| Note label | `d` | period | mask bit 7 | bit 6 | bit 5 | bit 4 | nearest real pitch |
| --- | --- | --- | --- | --- | --- | --- | --- |
| AF | `$47` | 143 | 24.45 | 48.89 | 97.78 | 195.56 Hz | G |
| A | `$3F` | 135 | 25.89 | 51.79 | 103.57 | 207.15 | G♯ −4 ¢ |
| BF | `$37` | 127 | 27.52 | 55.05 | 110.10 | 220.20 | A +2 ¢ |
| B | `$30` | 120 | 29.13 | 58.26 | 116.52 | 233.04 | A♯ |
| C | `$29` | 113 | 30.93 | 61.87 | 123.74 | 247.48 | B |
| CS | `$23` | 107 | 32.67 | 65.34 | 130.68 | 261.36 | **C −2 ¢** |
| D | `$1D` | 101 | 34.61 | 69.22 | 138.44 | 276.88 | C♯ −2 ¢ |
| EF | `$17` | 95 | 36.80 | 73.59 | 147.19 | 294.37 | D |
| E | `$12` | 90 | 38.84 | 77.68 | 155.36 | 310.72 | D♯ −2 ¢ |
| F | `$0D` | 85 | 41.13 | 82.25 | 164.50 | 329.00 | E −3 ¢ |
| FS | `$08` | 80 | 43.70 | 87.39 | 174.78 | 349.56 | F +2 ¢ |
| G | `$04` | 76 | 46.00 | 91.99 | 183.98 | 367.96 | F♯ −10 ¢ |

Every label sounds one semitone below its name (note the `CS` row landing on 261.36 Hz — middle C).
The scale is internally consistent to within ±10 cents, so the transposition is a property of the
chosen delay constants against this clock, not sloppiness. `INFERRED` for the interpretation.

**Note durations.** `ORGANL` sets `DUR` (16-bit) and `ORGAN1` decrements it once per sample, so a
note lasts `DUR × (72+d)` cycles. The `T*` constants (`VSNDRM1:22-33`) are `frequency-independent`
duration units: `T × (72+d) ≈ 2 478 000` for all twelve notes. Hence

```
quarter note at TACC=4  = 2 478 000 / 8 cycles = 346 ms   (≈173 BPM)
quarter note at PHANC=3 = 2 478 000 / 6 cycles = 462 ms   (≈130 BPM)
```

`TEMPB` is **not** reset between notes, so the oscillator phase runs continuously across the tune —
there are no note attacks at all, only instantaneous pitch changes. `SOURCE_CONFIRMED`.

**Tune 1 — "PHANTOM"** (`VSNDRM1:1020-1023`), 3 notes, 2.77 s:

| mask | note | `DUR` | actual f | ms |
| --- | --- | --- | --- | --- |
| `$7F` | D `$1D` | 4 091 | 69.22 Hz | 461.7 |
| `$7F` | CS `$23` | 3 861 | 65.34 Hz | 461.7 |
| `$FE` | FS `$08` | 20 618 | 43.70 Hz | 1 843.2 |

**Tune 2 — "TACCATA"** (`VSNDRM1:1025-1059`), 34 notes, **16.80 s**. This is the opening of Bach's
Toccata and Fugue BWV 565 (the mordent figure A–G–A, the descending run, the pedal D). Written in D
minor; sounds in C♯ minor. Selected rows (`INFERRED` arithmetic):

| # | mask | note | `DUR` | actual f | ms | source label |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | `$3E` | A | 574 | 103.57 | 86.6 | A3 1/16 |
| 2 | `$7C` | G | 1 023 | 91.99 | 86.9 | G2 1/16 |
| 3 | `$3E` | A | 11 490 | 103.57 | 1 733.3 | A3 5/4 |
| 4 | `$7C` | E | 3 444 | 77.68 | 346.4 | E2 1/4 |
| 7 | `$7C` | D | 10 738 | 69.22 | 1 211.9 | D2 7/8 |
| 27 | `$1F` | CS | 5 792 | 261.36 | 692.5 | CS4 1/2 |
| 34 | `$FE` | D | 24 548 | 34.61 | 2 770.6 | D1 2 |

**Organ command protocol.** `SOURCE_CONFIRMED`:

* `ORGANT` (`$1B`) does nothing but `DEC ORGFLG` → `$FF`, then returns. The **next** command's
  value is consumed by `ORGNT1` as a tune number (1 = PHANTOM, 2 = TACCATA). `ORGTAB` is a chain of
  `<length byte><length bytes of note data>`, terminated by a zero length.
* `ORGANN` (`$1C`) sets `ORGFLG = 3` and needs **three further commands**: two supplying the high
  and low nibbles of `OSCIL` (each as `command − 1`, shifted in by `ASLB ×4 / ABA`), then one
  supplying the note index into `NOTTAB` (values > 11 are forced to 0). The note then plays with
  `DUR = $FFFF` in an infinite `BSR ORGAN / BRA` loop. Between commands the CPU parks on `BRA *`.
* The game only ever uses the tune path. `amode1.src:155-166` writes `$3F` (release), `$24`
  (= `$1B`, arm organ), a short `DECB` delay, `$3F`, then `$3E` or `$3D` (tune 1 or 2).

### 6.9 Parameter-transfer and helper routines

`SOURCE_CONFIRMED`: `TRANS` (`$FB0A`) is a byte-copy loop used by `VARILD` and `WVTRAN`;
`ADDX` (`$FD21`) is an 8-bit add to X used for every table index; `GWLD`'s multiply-by-7 is
`TAB / ASLB / ABA / ABA / ABA` and `VARILD`'s multiply-by-9 is `TAB / ASLA / ASLA / ASLA / ABA`.

---

## 7. Randomness

`SOURCE_CONFIRMED` (`VSNDRM1:270-277`, `309-316`, `370-377`). Two variants of the same 16-bit
right-shifting feedback register `{HI:LO}` at `$09:$0A`:

**Variant A — pure LFSR** (used by `LITE`, `APPEAR`, `NOISE`/`TURBO`):

```
LDAA LO ; LSRA ×3 ; EORA LO ; LSRA        ; carry = bit3(LO) XOR bit0(LO)
ROR HI ; ROR LO                            ; shift {HI:LO} right, carry into bit 15
                                           ; the bit shifted OUT of LO is the random output
```

Recurrence `a_n = a_{n−13} XOR a_{n−16}`, i.e. polynomial x¹⁶ + x³ + 1. This trinomial is **not
primitive** (no primitive trinomial of degree 16 exists), so the sequence is not maximal length. By
exhaustive iteration the period is **57 337** for every non-zero seed tested, with no tail
(`INFERRED`, by simulation). At the `TURBO` sample rate that is 3.6 s of noise before repetition —
inaudible as a repeat, but a reimplementation that substitutes `Math.random()` will sound slightly
different in the very short bursts.

**Variant B — output-coupled** (used only by `FNOISE`, i.e. background, thrust and explosions):

```
TAB          ; B = A = the CURRENT DAC OUTPUT LEVEL, not LO
LSRB ×3 ; EORB LO ; LSRB
ROR HI ; ROR LO
```

The feedback bit is `bit3(current DAC level) XOR bit0(LO)`. The generator is chaotically coupled to
the audio it is producing. `SOURCE_CONFIRMED` — this is not a typo in the recovered source; the
`TAB` is essential, `A` holds the level about to be slewed.

**Consecutive targets are correlated.** `FNOISE` uses the whole byte `LO` as its slew target, and
`LO` only shifts by one bit per draw: `LO' = (bit0(HI) << 7) | (LO >> 1)`. Successive targets are
therefore *halves* of each other plus a new top bit, not independent uniform draws. Measured mean
segment length for `FMAX = 1` is 64 slew steps, versus 85 for genuinely independent uniform targets
(`INFERRED`, by simulation). Reimplementing `FNOISE` with white random targets makes the drone
noticeably higher-pitched and less "burbling". **Implement the shift register literally.**

**Seeding.** `HI = $3C` at reset; `LO` is uninitialised RAM (`VSNDRM1:182`). Defender's noise is
therefore not bit-identical between power-ups. `SOURCE_CONFIRMED`.

---

## 8. Persistence, looping, and interruption

### 8.1 Which sounds are one-shot and which loop

`SOURCE_CONFIRMED`:

* **Infinite until interrupted:** `BG1` (`$0F`), `THRUST` (`$16`) — `FNOISE` with `FDFLG = 0` never
  reaches a terminating condition. `SP1` (`$0E`) — `SP1LP JSR VARI / BRA SP1LP`. `ORGANN`'s note
  (`$1C`) — `ORGNN4 BSR ORGAN / BRA ORGNN4` with `DUR = $FFFF`. `BG2` (`$10`) — `BG2LP INC GDCNT /
  JSR GEND61 / BRA BG2LP`.
* **Long but finite:** everything else. Several run for many seconds (§6) and are simply preempted
  in practice.
* **Return path:** any routine that returns lands at `IRQ3`, which jumps into `BG1` or `BG2` if a
  background flag is set, otherwise spins at `BEQ *`.

### 8.2 The background drone

`MULTI_SOURCE_CONFIRMED`. **Yes, Defender has a continuous background sound, and it is `BG1`.**

`BG1` (`$F913`, `VSNDRM1:338-342`) sets `BG1FLG = 1` and falls into `FNOISE` with `FMAX = 1`,
`DSFLG = 0`, `FDFLG = 0` — a ≈230 Hz random-walk rumble at full DAC swing that never ends. Because
`BG1FLG` stays set in RAM, `IRQ3` re-enters `BG1` after **every** one-shot sound completes. That is
the mechanism by which the drone appears to duck under effects and return.

How it is switched:

| Event | Command | Evidence |
| --- | --- | --- |
| Player releases thrust | `$0F` (`BG1`) | `defa7.src:742-748` (`SNDS00`: thrust bit clear, `THFLG` set → `LDB #$0F`) |
| Player presses thrust | `$16` (`THRUST`) | `defa7.src:749-756` (`SNDS01` → `LDB #$16`, also stored into `THFLG`) |
| Player blown up / game over | `$13` (`BGEND`) | `defa7.src:1384-1386`, `defa7.src:1428-1430` |

`BGEND` (`$FB1E`) clears both `BG1FLG` and `BG2FLG` and returns, so `IRQ3` then idles silently.

**The drone does not exist until the player thrusts once.** `$0F` is sent *only* by the
thrust-release path, and the thrust check in `SNDSEQ` only runs when no sequenced sound is active
(`LDA SNDTMR / BEQ SNDS00`, `defa7.src:723-724`). `SOURCE_CONFIRMED`; a reimplementation that starts
the drone at wave start will be wrong.

`BG2` (`$FB34`) is the *alternative* background: the `TRBV` turbine, whose pitch is set by
`FOFSET = ~(4 × BG2FLG)` (`VSNDRM1:680-684`), i.e. period `$7F − 4·BG2FLG`, with `BG2FLG` advanced
1…29 by the `BG2INC` command. At `BG2FLG = 1` the period is 123 and at 29 it is 11 — a turbine that
spins up as the game progresses. **Defender never sends `$10`.** `BG2` is unreachable in gameplay;
it is Sam Dicker's general-purpose Williams background engine, exercised only by the operator audio
test. `SOURCE_CONFIRMED` (grep of every `*.src` in the game source found no `$10` sound number and
no other writer of `BG2FLG`).

### 8.3 The two stateful commands

`SOURCE_CONFIRMED` (`VSNDRM1:920-926`). The IRQ handler zeroes `SP1FLG` unless the command is `$0E`,
and zeroes `B2FLG` unless the command is `$12`. Therefore:

* Repeating `$0E` **advances** the spinner pitch (§6.3); any other command in between resets it to
  the bottom.
* Repeating `$12` **continues** the bonus sweep: `BON2` (`VSNDRM1:716-722`) checks `B2FLG` and, if
  already set, does `JMP GEND50` — re-entering the frequency-offset step of the *already loaded*
  `BONV` rather than reloading it. `GEND` also stops the sound early when `B2FLG` is set
  (`VSNDRM1:824-825`), so each `$12` produces exactly one short rising blip.
* `IRQ3` additionally zeroes `B2FLG` whenever a background is active (`VSNDRM1:957-958`).

### 8.4 Game-side priority and sequencing

`SOURCE_CONFIRMED` (`defa7.src:659-757`). The sound board has no priority; the game does.

Table format (`defa7.src:660-664`): `SNDPRI, N × (REPCNT, SNDTMR, SND#), 0`.
`SNDTMR` is in units of 16 ms; `REPCNT` is how many times that command is re-sent at that interval;
the comment is explicit that priority is `HI=HI, INTERRUPTABLE BY EQUALS`.

```
SNDLD   CLR THFLG                  ; loading any sound cancels the thrust latch
        LDA ,X ; CMPA SNDPRI ; BLO reject     ; accept if priority >= current
        STA SNDPRI ; STX SNDX ; SNDTMR = SNDREP = 1
SNDSEQ  ... decrement SNDTMR; on expiry emit the next (REPCNT,SNDTMR,SND#) triple via SNDOUT
        when REPCNT byte = 0: SNDPRI = 0 and NO command is sent
```

The last point matters: **when a game-side sound sequence ends, nothing is sent to the sound board.**
The sound board keeps playing whatever it was last told, to completion or forever. Combined with §8.1
this explains why sounds whose free-running length is 5–20 s are perfectly reasonable: they are
statistically always preempted, and when they are not you hear the tail.

---

## 9. Reimplementation notes (Web Audio / AudioWorklet)

**9.1 Model the DAC, not the effect.** Implement one `AudioWorkletProcessor` that runs the original
algorithms at their natural rate and resamples to the context rate. Concretely: keep a cycle
accumulator, and every time a routine writes `SOUND`, hold that 8-bit value for the exact number of
E cycles the routine spends before the next write. Convert code `v` to a sample as
`(v − 128) / 128`. Do **not** try to approximate the engines with oscillator nodes — the
`GWAVE` per-sample delay-loop quantisation, the `VARI` 14-cycle grid and the `FNOISE` slew limiter
all produce characteristic aliasing that *is* the Defender sound.

**9.2 Rate conversion.** *(Corrected 2026-08-09 — this previously said "the maximum tone frequency is
`GWAVE`'s 35.8 kHz pass rate", conflating the per-sample DAC write rate with the fundamental and
overstating the latter by roughly 12×.)* The two ceilings are different quantities:

- **Maximum DAC write rate: `FNOISE`'s 29.8 kHz** (30 cycles per slew step). `GWAVE` reaches
  28.9 kHz at `GPER = 1`, just below it.
- **Maximum tone fundamental: `GWAVE`'s 3 013 Hz pass rate** (`L = 8`, `GPER = 1`). No engine on this
  board produces a pitched tone above ~3 kHz.

Run the emulation at a fixed internal rate of at least 1.79 MHz (E × 2) if you want the square-edge
timing right, or generate a zero-order-hold signal at the exact write instants and low-pass it. The
1.79 MHz recommendation stands, but it is justified by **edge timing** — the engines toggle the DAC
on single-cycle boundaries — not by any 35 kHz tone. `INFERRED`.

**9.3 Anti-aliasing.** The real board has an analogue reconstruction filter that is not documented
here (§11 Q1). Without *some* low-pass, a direct port will be harsher than the cabinet. A first-order
low-pass in the 4–8 kHz region is a reasonable placeholder; label any such choice as a deviation.
`INFERRED`.

**9.4 Single voice, hard cut.** One command at a time. On a new command, stop everything instantly,
reset the "stack", and start the new routine. Do not add a release envelope.

**9.5 Preserve the shift register.** Implement both variants in §7 literally, including the
`FNOISE` coupling to the current DAC level and the correlated target sequence. Seed `HI = $3C` and
`LO` randomly.

**9.6 Preserve the unsigned wraparound in `WVDECA`.** Use `(x - y) & 0xFF`, not clamping.

**9.7 Preserve the two table overruns** (`HBEV` reading `SPNR`, `ED17` reading `COOLDN`). They are
part of the shipped ROM's behavior.

**9.8 Drive it from the game's tables, not from an ad-hoc event list.** Implement `SNDLD` /
`SNDSEQ` / `SNDOUT` as written: priority byte, `REPCNT`, 16 ms `SNDTMR`, no stop command at sequence
end, `THFLG` cleared on every load. Half of Defender's audio texture is the *interruption pattern*,
not the individual sounds.

**9.9 Clock choice.** Use E = 894 886.25 Hz. If you want the organ at concert pitch you must
transpose it up a semitone — but that is a deviation from the original hardware and should be a
switchable option, not a default.

---

## 10. Notable defects in the shipped code

**D-1. The laser-ball bonus sound never plays.** `SOURCE_CONFIRMED`. `defb6.src:813-821`:

```
LNDFX0  CLRD                     ; A = 0, B = 0
        STD    OYV,X
        STD    OYV,Y
        LDA    OY16,Y
        CMPA   OY16,X
        BLS    LNDFX1
        DEC    OY16,Y
        LDA    #$12              ; ← should be LDB
        JSR    SNDOUT            ; SNDOUT takes the command in B
```

`SNDOUT` reads the command from **B** (`COMB / ANDB #$3F / STB SOUND`, `defa7.src:701-703`). B is
zero here, cleared by `CLRD` three instructions earlier and never touched again. The call therefore
transmits `~0 & $3F = $3F` — the idle pattern — twice, producing no CB1 edge and no interrupt.
Command `$12` (`BON2`) is dead in Defender, and with it the whole `BONV` vector and `BONSND`
pattern. The identical `LDA #$12` appears in the un-modernised
`historicalsource-defender/DEFB6.SRC:820`, so this is original, not a transcription error.

**D-2. `HBTSND` is orphaned data.** `SOURCE_CONFIRMED`. The 14-byte "HUNDRED POINT SOUND" pattern at
`$FF62` is not referenced by any `SVTAB` entry.

**D-3. Two `SVTAB` pattern lengths overrun their tables** (§6.1). `SOURCE_CONFIRMED`.

**D-4. `SCREAM` writes one byte past `SRMEND`** (§4). `SOURCE_CONFIRMED`.

**D-5. Diagnostic LED writes command a sound.** `INFERRED`. `romf8.src:297-299` and `306-308` write
`$40` / `$80` to `$CC02` to light the "bad RAM" / "bad ROM" LEDs. Both leave sound bits 0–5 at zero,
which decodes to command `$1F` (`QUASAR`). A board failing its power-on test will emit the
astro-land whoop.

**D-6. The `EORA #$3F` in the audio test is dead.** `SOURCE_CONFIRMED`, `romc0.src:515`. `PLAYB`
does its own complement, so the pre-complement at `SOUND5` is discarded by the following
`LDB #KILSND`.

---

## 11. Open questions

**Q1. The analogue output stage.** `UNRESOLVED`. Neither the source nor the MAME driver describes
the reconstruction filter, op-amp gain, or volume-pot range after the MC1408. This directly affects
perceived brightness of `LITE`, `APPEAR`, `RADIO` and every `GWAVE` sound near the top of its
frequency pattern. *Resolution:* the Williams Defender operator/service manual schematic sheet for
the sound board (part A-5342-xxxxx), or a scope/spectrum capture from a real board.

**Q2. Relative loudness across engines.** `UNRESOLVED`. The DAC swings are known (§6) but whether
the mix is perceptually balanced on the real board depends on Q1 and on the cabinet's single volume
control. *Resolution:* recorded gameplay capture from a known-good cabinet, level-matched.

**Q3. Exact behavior at power-up before the first thrust.** `INFERRED` only. My reading is that the
board idles silently at `BEQ *` from reset until the first command. *Resolution:* MAME trace of
`$0400` writes over the first ten seconds of attract mode.

**Q4. `LO` seeding.** `UNRESOLVED`. `LO` is uninitialised, so the noise sequence differs between
power-ups and between emulator and hardware. *Resolution:* not resolvable in principle; pick a
random seed.

**Q5. Does the main board ever read the sound handshake (CB2 → `$C80C` bit 7)?** `UNRESOLVED`. I did
not find a read of that bit in the recovered game source. *Resolution:* grep the diagnostics for a
`BITB #$80` against `$CC00`/`$C80C` and trace the RAM/ROM-OK path in `romf8.src`.

**Q6. The `$F700` checksum/relocator block.** `INFERRED`. `CKORG EQU $F700` (`VSNDRM1:12`) and
`END CKSUM` place the entry point below the 2 KB ROM window, so this block cannot be present in the
shipped `ic12` device. It appears to be shared boilerplate for larger Williams sound ROMs.
*Resolution:* compare against `VSNDRM2/3/4.SRC` in the same repository.

**Q7. The two checksum bytes.** **CLOSED 2026-08-09.** The shipped byte is **`$FF`**:
`roms/defender-redlabel/defend.snd[0] = 0xFF`, and `crc32(defend.snd) = fefd5b48`, which is exactly
the `video_sound_rom_1.ic12` entry at `williams.cpp:2002`. `historicalsource-williams-soundroms/
VSNDRM1.SRC:169`'s `FCB $FE` is the pre-final text — it would fail the sound ROM's own self-check.
`mwenge/src/vsndrm1.src`'s `$FF` is correct. Ledger `L-016`, adjudication `X7`; also recorded closed
at `OPEN_QUESTIONS.md` Q-07. *(This question was answered during the 2026-08-09 closure pass and
this paragraph was not back-edited; iteration 2 fixed that. Confidence labels live in
`EVIDENCE_LEDGER.md`.)*
