Skip to content

Commit f8eec8d

Browse files
committed
Wrap pitch mod 256 to match BBC byte register
The BBC's per-channel pitch is a single 8-bit byte and the OS uses the 6502 ADC for envelope PI updates, so adding a negative PI to a low pitch wraps up to a high pitch (e.g. 1 + -26 = 0xE7 = 231), not to a clamped low value. pitchToHz was clamping to 0..255 and the visualiser was plotting the unwrapped basePitch + offset directly, so any envelope that drove the pitch negative came out flat at the lowest BBC note instead of producing the wrapping pitch effect heard on real hardware. The BBC User Guide reference envelope 1,1,-26,-36,-45,255,255,255,127,0,0,0, 126,0 exposed this. Fix is centralised in pitchToHz (clamp -> & 0xff) so any caller can pass a signed integer; the visualiser now also computes wrapped pitches for its plot and auto-scale, so what's drawn matches what's audible.
1 parent 38a2272 commit f8eec8d

2 files changed

Lines changed: 12 additions & 6 deletions

File tree

src/envelope.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,13 @@ const PITCH_HIGH = [0xE7, 0xD7, 0xCB, 0xC3, 0xB7, 0xAA, 0xA2, 0x9A, 0x92, 0x8A,
286286
* right-shifts of the 10-bit divider, which causes the BBC's tuning to drift
287287
* slightly sharp of equal temperament in higher octaves — that quirk is
288288
* what we want to reproduce so playback matches the real machine.
289+
*
290+
* The input is wrapped mod 256 because the BBC's pitch register is a single
291+
* byte and the OS adds PI to it with the 6502's natural byte wrap. Negative
292+
* envelope offsets must wrap up to high pitches, not clamp at 0.
289293
*/
290294
export function pitchToHz(pitch: number): number {
291-
const p = clamp(pitch, 0, 255) | 0;
295+
const p = (pitch | 0) & 0xff;
292296
const fractional = p & 3;
293297
let count = p >> 2;
294298
let octave = 0;

src/visualizer.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,14 @@ export function render(
111111
ctx.textBaseline = "alphabetic";
112112
ctx.font = "12px system-ui, sans-serif";
113113

114-
// Pitch plot: absolute BBC pitch (basePitch + envelope offset). Auto-scale
115-
// around the base pitch with at least one semitone of headroom each side
116-
// and snap to semitone boundaries so gridlines land cleanly.
114+
// Pitch plot: absolute BBC pitch (basePitch + envelope offset, wrapped
115+
// mod 256 to match the BBC's single-byte pitch register). Auto-scale
116+
// around the actual pitches used with at least one semitone of headroom
117+
// each side, snapping to semitone boundaries.
118+
const wrappedPitch = (offset: number): number => (basePitch + offset) & 0xff;
117119
let minP = basePitch, maxP = basePitch;
118120
for (const s of samples) {
119-
const p = basePitch + s.pitchOffset;
121+
const p = wrappedPitch(s.pitchOffset);
120122
if (p < minP) minP = p;
121123
if (p > maxP) maxP = p;
122124
}
@@ -157,7 +159,7 @@ export function render(
157159
ctx.lineWidth = 2;
158160
for (let i = 0; i < samples.length; i++) {
159161
const x = xFor(i);
160-
const y = pitchY(basePitch + samples[i]!.pitchOffset);
162+
const y = pitchY(wrappedPitch(samples[i]!.pitchOffset));
161163
if (i === 0) ctx.moveTo(x, y);
162164
else ctx.lineTo(x, y);
163165
}

0 commit comments

Comments
 (0)