Skip to content

Commit 682eef2

Browse files
kieranhjclaude
andcommitted
Use the actual MOS envelope volume formula
att = (127 - amp) >> 3 — sixteen 8-wide BBC-amp cells mapped to the SN76489's 4-bit attenuation. Reproduces env1's trace exactly: AD=-10 from amp 121 produces atts 0, 2, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15 with skips at 1, 6, 11 — caused by the per-tick step crossing 8-wide cell boundaries unevenly, not by any non-linear lookup table. The `setChannelXVolume` MOS routine I was reading earlier is the static- amp path (BASIC SOUND with negative amplitude); envelope playback uses this simpler formula. Source: jsbeeb-mcp trace agent's debug notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 653b0c3 commit 682eef2

1 file changed

Lines changed: 14 additions & 16 deletions

File tree

src/audio.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,27 +93,25 @@ function getCtx(): AudioContext {
9393
}
9494

9595
/**
96-
* Map a BBC envelope amplitude (0..126) to an audible gain via the MOS
97-
* `setChannelXVolume` conversion at $eb0a (Toby Lobster S-s16 §3):
96+
* Map a BBC envelope amplitude (0..127) to an audible gain. The OS uses a
97+
* single linear-to-quasi-log conversion:
9898
*
99-
* SEC / SBC #$40 / LSR x3 / EOR #$0F (low 4 bits → SN attenuation)
99+
* att = (127 - amp) >> 3
100100
*
101-
* The BBC's working amplitude is a SIGNED byte (`channel0Volume`) spanning
102-
* $C0 (silent) through $3F (loudest), wrapping through zero. Our model
103-
* tracks unsigned amp 0..126; the corresponding chanVol is `(amp - $3F)`
104-
* mod 256. This shifts each att band by one amp unit relative to the
105-
* naïve `att = 15 - floor(amp/8)` — small but byte-accurate against the
106-
* MOS algorithm. Note: there's evidence (env1 trace's skipped att values
107-
* 1, 6, 11) that the OS has additional non-linear logic on top of this
108-
* algorithm, not yet reverse-engineered.
101+
* Sixteen 8-wide BBC-amp cells map to the SN76489's 4-bit attenuation.
102+
* This formula reproduces the env1 trace's "skipped attenuations" (1, 6,
103+
* 11) under AD=-10 exactly: amp values 121, 111, 101, ... yield att 0, 2,
104+
* 3, ... — atts 1, 6, 11 are never reached because the per-tick step
105+
* crosses cell boundaries unevenly. (The MOS `setChannelXVolume` routine
106+
* at $eb0a is a separate path used for static SOUND amps; envelope
107+
* playback uses this simpler linear formula.)
109108
*/
110109
function bbcAmpToGain(amp: number): number {
111-
const chanVol = (amp - 0x3F) & 0xff;
112-
const att = (((chanVol - 0x40) & 0xff) >> 3) ^ 0x0f;
113-
const lowNibble = att & 0x0f;
114-
if (lowNibble >= 15) return 0;
110+
const a = amp < 0 ? 0 : amp > 127 ? 127 : amp;
111+
const att = (127 - a) >> 3;
112+
if (att >= 15) return 0;
115113
// SN76489 attenuation step is 2 dB. Loudest = 0, silent = 15.
116-
return Math.pow(10, (-lowNibble * 2) / 20) * 0.4; // 0.4 = output headroom
114+
return Math.pow(10, (-att * 2) / 20) * 0.4; // 0.4 = output headroom
117115
}
118116

119117
/**

0 commit comments

Comments
 (0)