Skip to content

Commit 07476e0

Browse files
committed
Add debugging traces and notes.
1 parent 00c6a06 commit 07476e0

5 files changed

Lines changed: 2084 additions & 0 deletions

File tree

debug/envelope-tool-debug-notes.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Envelope-tool ↔ BBC OS sound-chip debugging notes
2+
3+
These notes accompany the four reference traces in the jsbeeb repo root:
4+
`trace-env1.json`, `trace-env2.json`, `trace-env3.json`, `trace-env4.json`,
5+
plus `trace-vol-sweep.csv`. Each trace was captured by running the same
6+
`ENVELOPE` + `SOUND` pair on a real BBC OS 1.20 inside the jsbeeb headless
7+
emulator and logging every byte the OS sends to the SN76489.
8+
9+
The traces are the ground truth. envelope-tool's job is to produce the same
10+
sequence of register writes (modulo a small set of documented quirks) when
11+
fed the same `ENVELOPE` and `SOUND` parameters.
12+
13+
---
14+
15+
## 1. The two scales involved
16+
17+
The BBC and the SN76489 use different volume conventions.
18+
19+
| Scale | Range | Type | Where it appears |
20+
| ------------------ | ---------------- | ------------------------------- | ------------------------------------ |
21+
| BBC amplitude | `0..127` (7-bit) | **Linear** | `ENVELOPE` AA/AD/AS/AR, ALA, ALD |
22+
| SN76489 attenuation | `0..15` (4-bit) | **Logarithmic** (~2 dB per step) | What gets written to the sound chip |
23+
24+
The OS continuously holds a current 7-bit amplitude per channel and converts
25+
it to a 4-bit SN attenuation every time it issues a chip write.
26+
27+
## 2. The OS volume mapping
28+
29+
It is **not** a 128-byte lookup table. The OS uses a single arithmetic
30+
expression:
31+
32+
```
33+
attenuation = (127 - amplitude) >> 3 // integer divide, no rounding
34+
```
35+
36+
Equivalently, sixteen uniform cells eight BBC units wide:
37+
38+
| Attenuation (loudness) | BBC amplitude range |
39+
| ---------------------- | ------------------- |
40+
| 0 (loudest) | 120..127 |
41+
| 1 | 112..119 |
42+
| 2 | 104..111 |
43+
| 3 | 96..103 |
44+
| 4 | 88..95 |
45+
| 5 | 80..87 |
46+
| 6 | 72..79 |
47+
| 7 | 64..71 |
48+
| 8 | 56..63 |
49+
| 9 | 48..55 |
50+
| 10 | 40..47 |
51+
| 11 | 32..39 |
52+
| 12 | 24..31 |
53+
| 13 | 16..23 |
54+
| 14 | 8..15 |
55+
| 15 (silent) | 0..7 |
56+
57+
Verified empirically by the `trace-vol-sweep.csv` capture (envelope with
58+
`AD=-1`, `T=1`, swept from BBC amp 127 → 0): the SN attenuation increments
59+
exactly every eighth tick.
60+
61+
This linear-to-quasi-log mapping is the entire reason the BBC's "volume
62+
curve" looks non-linear when you observe register writes — the BBC
63+
amplitude is linear, the SN is log, and the conversion is integer
64+
truncation.
65+
66+
### Implication: which `ENVELOPE` step sizes will skip attenuations?
67+
68+
The OS writes to the chip only when attenuation **changes** between ticks.
69+
If a single tick changes BBC amplitude by enough to cross more than one
70+
8-wide cell, intermediate attenuations are skipped:
71+
72+
| Per-tick BBC amp delta | Will it skip attenuations? |
73+
| ---------------------- | -------------------------- |
74+
| 1..7 | Never (every cell entered) |
75+
| 8 | Sometimes (boundary aligned) |
76+
| 9..15 | Sometimes (one in N skipped) |
77+
| 10 | Skips ~1 in every 4 cells (see env1) |
78+
| ≥16 | Always skips ≥1 cell per tick |
79+
80+
env1 shows the canonical skip pattern (10 BBC units per tick → attenuations
81+
1, 6, 11 skipped). envelope-tool must implement the same "write only on
82+
change" logic, or it will emit extra writes envelope-tool didn't.
83+
84+
## 3. Trace format
85+
86+
Each entry in the JSON traces:
87+
88+
```js
89+
{
90+
cycle: 5604034, // CPU cycle, relative to first captured write (2 MHz clock)
91+
byte: 208, // raw byte written to &FE43 (SN76489 data port)
92+
byteHex: "0xd0",
93+
channel: 2, // SN76489 channel index (0/1/2 = tone, 3 = noise)
94+
kind: "vol", // one of: "vol" | "periodLo" | "periodHi" | "noise"
95+
field: 0, // decoded value (vol 0-15, period 0-15 lo/hi, noise 0-15)
96+
usedLatch: false // true if this byte continued a previously-latched register
97+
}
98+
```
99+
100+
For tone period: `period = (periodHi << 4) | periodLo` (12-bit period, 4 lo
101+
bits then 6 hi bits over the bus).
102+
103+
### BBC channel → SN76489 channel mapping
104+
105+
| BBC `SOUND` channel | SN76489 channel | Notes |
106+
| ------------------- | --------------- | ---------------------------- |
107+
| 0 | 3 | Noise channel |
108+
| 1 | 2 | Tone (this is what env1 uses) |
109+
| 2 | 1 | Tone |
110+
| 3 | 0 | Tone |
111+
112+
Yes, the order is reversed. envelope-tool must apply this mapping.
113+
114+
## 4. The traces, summarised
115+
116+
| Trace | ENVELOPE | SOUND | Channel | Writes | What it stresses |
117+
| -------------------- | ------------------------------------------------- | -------------------- | -------------- | ------ | -------------------------------------- |
118+
| `trace-env1.json` | `1,8,1,-1,1,1,1,1,121,-10,-5,-2,120,-1` | `SOUND 1,1,100,40` | tone (SN 2) | 48 | Volume **skips** + pitch wobble (PI±1) |
119+
| `trace-env2.json` | `2,1,14,-18,-1,44,32,50,6,1,0,-2,120,126` | `SOUND 0,2,0,16` | noise (SN 3) | 173 | Pitch envelope writes to **noise control register** |
120+
| `trace-env3.json` | `3,4,0,0,0,1,1,1,126,-4,-2,-4,126,110` | `SOUND 0,3,7,100` | noise (SN 3) | 16 | Volume-only, slow `T=4` |
121+
| `trace-env4.json` | `1,1,0,0,0,0,0,0,127,-3,-2,-4,126,80` | `SOUND 0,1,6,30` | noise (SN 3) | 16 | Volume-only, fast `T=1`, **decay→sustain transition** at ALD=80 |
122+
| `trace-vol-sweep.csv`| `1,1,0,0,0,1,1,1,126,-1,-1,0,126,1` | `SOUND 0,1,6,200` | noise (SN 3) | 17 | Sweep amp 126→0 to derive cell boundaries |
123+
124+
## 5. Behaviours envelope-tool must reproduce
125+
126+
### 5.1 Volume conversion (the big one)
127+
128+
Every tick:
129+
130+
1. Update BBC amplitude (signed add of AA/AD/AS/AR, clamp to 0..127).
131+
2. Compute `att = (127 - amp) >> 3`.
132+
3. If `att` differs from the previously written value, send `0x90 | (channel << 5) | att` to the chip (the high bit and `0x10` mark a volume write; channel is the SN channel, not the BBC channel).
133+
134+
### 5.2 Pitch envelope is per-tick, not per-step
135+
136+
Pitch sections (PI1/PI2/PI3 with PN1/PN2/PN3 step counts) advance one
137+
unit per **envelope tick** (`T` × 10 ms), not per attenuation change. The
138+
OS writes a new period every time the section's pitch value changes.
139+
140+
For tone channels the pitch byte goes through the SN's two-byte period
141+
protocol: `periodLo` (latched), then `periodHi` ~400 cycles later. For
142+
noise channels the same byte goes to the noise control register
143+
(`0xE0 | (value & 0x0F)`), which selects shift rate / white-vs-periodic
144+
mode rather than frequency.
145+
146+
**Crucial**: when all three PI values are zero, the OS writes nothing for
147+
pitch — no redundant register writes. envelope-tool should suppress
148+
writes when the value hasn't changed.
149+
150+
### 5.3 The note-start "double-write"
151+
152+
At note start, every trace shows the OS writing the initial attenuation
153+
twice in quick succession:
154+
155+
```
156+
cycle 0: att 0 (initial level after attack reaches ALA)
157+
cycle ~290: att 1 (first decay tick — fires immediately, ~145 µs later)
158+
```
159+
160+
This is the OS's normal startup flow (attack target → first envelope tick
161+
runs immediately) and is reproducible across all volume-only traces. It
162+
is probably inaudible. envelope-tool can match it or skip it; just be
163+
aware when diffing.
164+
165+
### 5.4 Decay → sustain transition
166+
167+
When BBC amplitude crosses `ALD`, the per-tick rate switches from `AD`
168+
to `AS`. env4 demonstrates: 30 ms cadence (3 ticks/cell at `AD=-3`)
169+
becomes 40 ms cadence (4 ticks/cell at `AS=-2`) at attenuation 7→8,
170+
which corresponds to BBC amp ≈ 80 = `ALD`.
171+
172+
### 5.5 Pitch envelope continues during silence
173+
174+
Once attenuation reaches 15, the volume engine stops writing — but the
175+
pitch envelope continues to advance and emit register writes for the
176+
remainder of the SOUND duration. env1 and env2 both show this. env3
177+
and env4 don't, because their pitch envelopes are zero so there's
178+
nothing to write. envelope-tool must keep ticking pitch on silent
179+
channels.
180+
181+
### 5.6 SOUND queue startup latency
182+
183+
In absolute cycle terms, every SOUND command in these traces fires
184+
**~1 second after the BASIC line is typed**. The emulator's typing
185+
finishes, the OS schedules the note via the sound queue, and the actual
186+
register writes begin a noticeable delay later.
187+
188+
For envelope-tool this matters perceptually: if it begins emitting writes
189+
the instant the user clicks "play," the attack will start sooner than the
190+
real BBC. For trace diffing this can be ignored (cycles in the JSON are
191+
already rebased to the first note write).
192+
193+
## 6. Suggested debug procedure
194+
195+
1. **Implement the volume formula first** (`att = (127 - amp) >> 3`).
196+
Diff against `trace-env3.json` and `trace-env4.json`. They are
197+
volume-only and the cleanest tests.
198+
2. **Add the decay→sustain transition** so env4's timing change at
199+
attenuation 7 reproduces.
200+
3. **Add "write only on change"** semantics. Diff against env1: you must
201+
see the same skipped attenuations (1, 6, 11) when `AD=-10`.
202+
4. **Implement pitch envelopes for tone channels**. Diff against env1's
203+
period writes (oscillating between 0xECD and 0xECA).
204+
5. **Implement pitch envelopes for noise channels** by writing to the
205+
noise control register. Diff against env2.
206+
6. **Verify the BBC↔SN channel reversal** (BBC 0 ↔ SN 3, BBC 1 ↔ SN 2,
207+
etc.). All four traces use this mapping.
208+
209+
The diff doesn't have to be byte-for-byte cycle-perfect — small cycle
210+
deltas (a few hundred cycles per write) are normal because the OS doesn't
211+
schedule writes on exact cycle boundaries. Aim for **same byte values in
212+
the same order** with cycle deltas within ±5%.
213+
214+
## 7. Reproducing the traces
215+
216+
The capture tool is `tools/sound-trace.js`. Command shape:
217+
218+
```sh
219+
node tools/sound-trace.js \
220+
--cmd "ENVELOPE 1,8,1,-1,1,1,1,1,121,-10,-5,-2,120,-1" \
221+
--cmd "SOUND 1,1,100,40" \
222+
--run-cycles 12000000 \
223+
--skip-cycles 5000000 \
224+
--relative-cycles \
225+
--pretty \
226+
--out trace.json
227+
```
228+
229+
`--skip-cycles` drops boot-time noise (the OS silences channel 0 at startup).
230+
`--relative-cycles` rebases the first remaining write to cycle 0.
231+
232+
The tool requires `npm install` to have been run in the jsbeeb repo (it
233+
pulls in `sharp` via `MachineSession`).
File renamed without changes.

0 commit comments

Comments
 (0)