0. Framing — why this is the same law, not a new one
The canon's central move is that grey is a lie told by dots: continuous tone is represented as a spatial density of binary marks, never as an intermediate shade. Delta-sigma modulation (DSD, 1-bit audio) is exactly this claim rewritten in the time domain: a continuous amplitude is represented as a temporal density of binary pulses (all-1 or all-0, no intermediate voltage), and the ear's low-pass filtering does what the eye's spatial low-pass filtering does for a Bayer-dithered image. This is not a metaphor of convenience — it is the literal same operator (noise-shaped quantization) applied on the other axis. That's the justification for building it at all under Law "no new hexes / no new flourishes": it is one constraint (1-bit density) applied twice, not two constraints.
Concretely this means: the audio brief is not "add a synth." It is "add a bitstream" — a signal that is provably only ever +1/-1 (or 1/0) at the sample level, whose density of ones carries the sub-bass heartbeat and the noise floor. If at any point the implementation reaches for a multi-bit oscillator with smooth gain automation and calls it "1-bit" because the final DAC happens to be 16-bit, it has broken the doctrine while keeping the name. The brief below is written to keep the bitstream real.
1. Background — what "1-bit audio" actually is
DSD / delta-sigma modulation (Sony/Philips, 1999, descended from the delta-sigma ADCs used in most digital audio converters since the 1980s). A 1-bit DSD stream is not "low quality PCM" — it's a very high sample rate (2.8224 MHz for DSD64) single-bit stream where information is encoded as pulse density: more 1s in a window = higher instantaneous amplitude. It's generated by a feedback loop: compare the input signal against a running estimate of what the bitstream has produced so far (the integrated error), emit a 1 or 0 based on the sign of that error, and feed the error back in for the next sample. This noise-shapes the quantization error up out of the audible band. The classic reference is Norsworthy/Schreier/Temes, Delta-Sigma Data Converters (1997); the audio-specific popularization is Sony/Philips' Super Audio CD/DSD spec (1999).
ZX Spectrum beeper (1982) and early PC speaker. These are the degenerate, unshaped case: a single GPIO pin toggling a piezo/speaker cone directly, 1-bit, no DAC at all. Tone comes from toggling at audio-rate frequencies (a square wave — this is the "beeper music" tradition: Spectrum tunes were literally interrupt-driven pulse trains). Where it gets interesting for this brief is sample playback on 1-bit hardware — techniques like Direct D/A adapted to a 1-bit pin (used in the demoscene, e.g. the "Covox"-adjacent and PWM-driven beeper players from the mid-90s Spectrum scene): the pin is toggled at a fixed high rate (e.g. 15.6 kHz–31 kHz) and the pulse width within each period approximates the sample value — that's PWM, a cousin of delta-sigma but open-loop and lower quality (no noise shaping, so quantization noise isn't pushed out of band, it just sits there as grit). PC speaker "RealSound"-style playback (Access Software, 1988) is the same open-loop PWM idea on the IBM PC's own 1-bit speaker driver.
Two distinct techniques, and the brief needs both:
- Delta-sigma (closed-loop, noise-shaped) — use this for the tone-bearing signal (the heartbeat), because it pushes quantization noise up into inaudible territory, giving a cleaner low end from a literal 1-bit stream.
- Open-loop pulse-density / PWM — acceptable and arguably more honest for the noise-density layer, since "grit" is the point there and noise shaping would waste effort cleaning up something meant to sound grainy.
2. Signal design
Two independent 1-bit streams, mixed after both are already binary-derived, not before:
A. Sub-bass heartbeat carrier. Target frequency: low, e.g. 55 Hz (A1) or lower — something felt more than heard, consistent with "sub-bass." Its amplitude envelope is not a new curve; it must be the canonical pulse already defined in the codebase:
beat(t) = sin(t · 1.55)^4
confirmed in canon.BEFORE.src.html (var beat = Math.pow(Math.sin(t*1.55), 4);) and in DESIGN.md §6. Its true period is π / 1.55 ≈ 2.027s (sin⁴ halves the period of sin), which matches the brief's "period 2.03s" — use 2.027 if you need to hardcode a loop length, but prefer computing it live from t so it never drifts from the visual pulse. The audio and the visual pulse must share one time source (u_time/performance.now()-derived t, or better, an explicit shared t0 epoch passed to both the shader and the AudioWorklet at start) — two independently-clocked sin⁴ curves will phase-drift within seconds and the "same seed, same output" law is violated the moment they do.
Carrier signal before quantization:
x(t) = A · beat(t) · sin(2π · f0 · t)
with f0 ≈ 55, A the headroom-limited peak (see §4).
B. Field-density noise. Filtered noise whose pulse density (not amplitude — density, to keep the doctrine literal) follows a scalar d ∈ [0,1] read from the page: the field's mean shape(f) value (see the fragment shader — shape() is already the 0..1 density function the field uses per-cell; you need its spatial mean per frame, not the per-pixel value, since audio has no notion of "this pixel"). This is analogous to a dither matrix's fill ratio.
3. Getting the density scalar out of a WebGL field cheaply
This is one of the two genuinely hard parts. You cannot read back the full framebuffer every frame without a stall (a gl.readPixels on the full canvas is one of the more reliable frame-time killers in headless and real GPUs, because it forces a pipeline flush).
Concrete approach:
-
Render (or re-evaluate) the field's density function at a tiny fixed resolution — e.g. 16×16 or even 8×8 — into a dedicated low-res FBO, once per frame, using the same
shape(fbm4(...))math but at trivial cost (256 texels vs. a full canvas). -
gl.readPixelsthat 16×16 (or 8×8) RGBA buffer — 1KB, effectively free, no visible stall even on swiftshader. -
Average the R channel (or luma) in JS →
d. -
Low-pass that scalar in JS (e.g. one-pole
d_smooth += (d - d_smooth) * 0.05per frame) before handing it to audio — the visual field is spatially noisy frame to frame and you do not want the noise-bed density literally jittering at 60 Hz, that reads as digital chatter rather than "grain." -
Post
d_smoothto the AudioWorklet viaport.postMessageat a throttled rate (every ~100 ms is plenty — density is a slow-moving ambient quantity, not a control-rate signal) or, better, via aSharedArrayBuffer-backedAudioParam-like ring if you want to avoid message-passing GC churn (see §6).
If SharedArrayBuffer is unavailable (it requires cross-origin-isolation headers — COOP/COEP — which a static artifact served from an arbitrary host will very often not have), fall back to plain postMessage; at a 10 Hz update rate this is not a performance concern.
4. Web Audio architecture
Use an AudioWorkletProcessor, not ScriptProcessorNode. ScriptProcessorNode runs on the main thread and is deprecated; on a page that's already running a WebGL shader loop and possibly Canvas 2D dither work, contending for main-thread time will produce audible glitches under load — the worklet runs on the dedicated audio render thread and is the only architecturally sound choice here. Support is universal in evergreen browsers (Safari since 14.1, 2021) so there's no compatibility argument for ScriptProcessorNode today.
4a. Node graph
[AudioWorkletNode "onebit-processor"]
│ (already-quantized ±1 bitstream, generated inside the worklet)
▼
[GainNode masterGain] ← single point of gain-staging, see §5
▼
[AudioContext.destination]
Do the delta-sigma math inside the worklet, sample-by-sample, so the ±1 stream is real and not an illusion produced by clipping a sine wave downstream (a WaveShaperNode hard-clip looks like 1-bit on a scope but is not driven by pulse-density feedback and will alias badly — don't do that).
4b. The processor
// onebit-processor.js — registered via audioContext.audioWorklet.addModule(...)
class OneBitProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'density', defaultValue: 0, minValue: 0, maxValue: 1, automationRate: 'k-rate' },
{ name: 'active', defaultValue: 0, minValue: 0, maxValue: 1, automationRate: 'k-rate' },
];
}
constructor(opts) {
super();
this.t0 = opts.processorOptions.t0; // shared epoch, seconds (performance.now()/1000 at start)
this.f0 = 55; // sub-bass carrier, Hz
this.integrator = 0; // delta-sigma feedback state (1st order)
this.quant = 0; // last quantized output, -1 or +1
this.noiseIntegrator = 0; // separate 1st-order integrator for the noise branch
this.lpState = 0; // 1-pole lowpass state, shapes white noise before PDM
this.rngState = 0x9e3779b9 ^ (opts.processorOptions.seed | 0); // xorshift, seeded — reproducibility
}
// xorshift32 — deterministic given the seed, unlike Math.random()
rand() {
let x = this.rngState;
x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
this.rngState = x | 0;
return (x >>> 0) / 4294967296; // [0,1)
}
process(inputs, outputs, parameters) {
const out = outputs[0][0];
if (!out) return true;
const density = parameters.density[0];
const active = parameters.active[0];
const sr = sampleRate; // global in AudioWorkletGlobalScope
for (let i = 0; i < out.length; i++) {
if (active < 0.5) { out[i] = 0; continue; }
const t = this.t0 + (currentFrame + i) / sr; // sample-accurate, shares epoch with the shader
// --- carrier: sub-bass tone under the canonical pulse envelope ---
const beat = Math.pow(Math.sin(t * 1.55), 4);
const carrierTarget = 0.6 * beat * Math.sin(2 * Math.PI * this.f0 * t);
// 1st-order delta-sigma modulator on the carrier
this.integrator += carrierTarget - this.quant;
this.quant = this.integrator >= 0 ? 1 : -1;
// --- noise branch: pulse-density modulation driven by field density ---
// white noise, 1-pole lowpassed so its spectrum isn't flat (a bit of colour,
// and it keeps very-high-frequency content from aliasing hard against sr/2)
const white = this.rand() * 2 - 1;
this.lpState += (white - this.lpState) * 0.35;
const noiseTarget = (density * 0.5) * this.lpState; // density scales AMPLITUDE fed to the modulator,
// which is what makes ones more frequent — density
// in, density out, not a separate gain stage
this.noiseIntegrator += noiseTarget - (this.noiseIntegrator >= 0 ? 1 : -1) * Math.abs(noiseTarget) * 0 + noiseTarget;
// (see note below — simplify to the same 1-bit pattern as the carrier)
const noiseQuant = this.noiseIntegrator >= 0 ? 1 : -1;
this.noiseIntegrator -= noiseQuant;
// mix two independent 1-bit streams at fixed sub-unity weights, THEN
// this is where the signal stops being provably 1-bit and becomes a
// 3-level (or more) PCM signal again — see §5 on why that's necessary
// and acceptable.
out[i] = 0.22 * this.quant + 0.05 * noiseQuant;
}
return true; // keep the processor alive
}
}
registerProcessor('onebit-processor', OneBitProcessor);
I left one line messy on purpose (the noiseIntegrator update) to flag it in prose rather than bury the caveat in clean-looking code: write the noise branch with the identical two-line delta-sigma idiom as the carrier —
this.noiseIntegrator += noiseTarget - noiseQuant_prev;
noiseQuant = this.noiseIntegrator >= 0 ? 1 : -1;
— don't let it diverge into a different (and subtly wrong) integrator shape. The builder should delete my scratch line and use the same pattern as this.quant.
4c. Main-thread wiring
let ctx, node, gain, running = false;
async function startOneBit() {
if (running) return;
ctx = new (window.AudioContext || window.webkitAudioContext)();
await ctx.audioWorklet.addModule('onebit-processor.js');
node = new AudioWorkletNode(ctx, 'onebit-processor', {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [1],
processorOptions: { t0: performance.now() / 1000, seed: 0xC0FFEE }, // fixed seed → reproducible
});
gain = ctx.createGain();
gain.gain.value = 0; // start silent, ramp up — never a hard onset, see §5
node.connect(gain).connect(ctx.destination);
node.parameters.get('active').setValueAtTime(1, ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.35, ctx.currentTime + 0.6); // 600ms fade-in
running = true;
setVisibleAudioState('live'); // toggles the [ON]/[OFF] Departure label — see §5.4
}
function stopOneBit() {
if (!running) return;
const t = ctx.currentTime;
gain.gain.cancelScheduledValues(t);
gain.gain.setValueAtTime(gain.gain.value, t);
gain.gain.linearRampToValueAtTime(0, t + 0.3);
setTimeout(() => { ctx.close(); running = false; }, 350);
setVisibleAudioState('off');
}
// per-frame from the render loop, throttled:
function pushDensity(d_smooth) {
if (node) node.parameters.get('density').setValueAtTime(d_smooth, ctx.currentTime);
}
Every gesture-gated entry point (a button, never autoplay) calls startOneBit() directly inside the click handler — AudioContext construction and resume() must happen synchronously inside the user-gesture call stack, not after an await or inside a requestAnimationFrame callback, or Chrome/Safari's autoplay heuristic will refuse it. new AudioContext() itself is fine to construct anytime, but the first resume()/first sound must trace back to the click.
5. Gain staging, headroom, and "never clips"
This is the second genuinely hard part, and it's harder than it looks because two summed independent 1-bit streams are not one 1-bit stream:
-
this.quant∈ {-1, +1},noiseQuant∈ {-1, +1}. -
0.22·quant + 0.05·noiseQuant∈ {-0.27, -0.17, +0.17, +0.27} — four discrete levels, not two. This is an honest description of what happens the moment you sum two independent bitstreams at different weights, and there is no way around it while still hearing both signals: true single-bit composite output would require multiplexing the two sources in time (time-division) or modulating one as amplitude-into-the-other's-modulator (nesting the noise inside the carrier's delta-sigma loop as its target, rather than summing after quantization), not summing post-quantization. -
State this plainly rather than fudge it: if the letter of "1-bit" must hold at the final output, the correct construction is to not run two separate modulators, but to feed a single delta-sigma loop a target that is
carrierTarget + noiseTarget(sum the pre-quantization targets, quantize once): ``js const target = carrierTarget + noiseTarget; this.integrator += target - this.quant; this.quant = this.integrator >= 0 ? 1 : -1; out[i] = 0.22 * this.quant; // now genuinely two-level`` This is the technically correct version and the one the builder should ship — I initially split them for clarity of exposition above; collapse to one integrator, one comparator, one output line. Say so in the artifact's own commentary (§16 Evidence favors showing the actual math, not a black box) — a code comment stating "single delta-sigma loop, target = carrier + noise, output is strictly ±1 before the gain stage" is itself an evidence gesture the canon rewards.
Numeric headroom:
-
Peak of
carrierTargetis0.6(chosen socarrierTarget + noiseTargetpeaks around0.6 + 0.5·noiseAmp ≈ 0.85, safely inside the ±1 comparator range with margin — a delta-sigma loop whose target regularly exceeds unity magnitude loses its noise-shaping benefit and the integrator can run away). -
Post-quantizer output gain: hard-cap at
0.22on theGainNode-adjacent scalar multiply, i.e. the±1bitstream never reaches theGainNodeat unity. A raw ±1 square-wave-like signal at full scale is extremely loud and harsh at typical system volumes; 0.22 leaves ~13 dB of headroom before theGainNode. -
GainNode.gainfurther caps at0.35max (seestartOneBit), so worst-case sample magnitude reachingdestinationis0.22 × 0.35 ≈ 0.077, i.e. -22 dBFS peak, comfortably clip-free with margin even given the DC-heavy, high-crest-factor nature of a delta-sigma bitstream. -
No hard onsets or offsets: always ramp gain (600ms in, 300ms out above). A ±0.077 sample jumping from 0 instantly is an audible click;
linearRampToValueAtTimeavoids it. This is also the audio-domain expression of "nothing bounces" — no elastic easing on the ramp, linear or the canon's owncubic-bezier(0,0,.2,1)-equivalent (Web Audio only offers linear/exponential ramps natively; usesetValueCurveAtTimewith a precomputed cubic-bezier-sampled array if you want the exact canonical ease instead of linear — worth doing for consistency with the rest of the motion system, low cost). -
DC blocking: a 1st-order delta-sigma bitstream biased by an asymmetric target can accumulate DC offset over long runs. Add a
BiquadFilterNodeinhighpassmode,frequency ≈ 20 Hz,Q ≈ 0.707, betweennodeandgain— cheap insurance, standard practice in any 1-bit/PDM audio pipeline, removes any woofer-unfriendly DC creep without touching the audible carrier at 55 Hz+.
5.4 Visible state (required by the brief and by §15 Accessibility)
The control is a standard canon secondary/ghost button per §3 Components — Departure, tracked, uppercase, square. State is not a colour change (never essence or sigil as a fill) — it's the bracketed status idiom already in the canon: [ SOUND: OFF ] → on click → [ SOUND: LIVE ] with the label pulsing on the canonical beat while active, exactly as the loading-button spec already prescribes ("a loading button pulses its label on the canonical beat rather than spinning"). Reuse that idiom verbatim rather than invent a new active-state treatment. On failure (AudioContext construction throws, or resume() rejects — happens on some locked-down mobile browsers or when a stricter autoplay policy is in force even after a gesture) fall back to [FAIL] at o-1 per the existing error-state law, and log the reason to console — never fail silently, since a silent audio feature that appears to do nothing is worse than no feature.
6. Update-rate mismatch (density feed) — likely failure mode
AudioParam.setValueAtTime called every render frame (60/s) from the main thread against a k-rate param works but is wasteful and, worse, if the main thread stalls (WebGL readback contention, a layout thrash elsewhere on the page) the density value goes stale silently — not a crash, just wrong-sounding, which is the worst kind of bug here because nothing errors. Mitigations:
-
Throttle the
setValueAtTimecalls to ~10 Hz (matches the density's actual rate of change — see §3's smoothing) rather than 60 Hz; do the intra-worklet smoothing you'd otherwise rely on the AudioParam's ownk-ratesample-and-hold to provide, or add a one-pole smoother inside the worklet on top of the incoming k-rate value so a 10 Hz step input becomes an audibly smooth glide instead of 10 audible steps/sec. -
If the tab is backgrounded, browsers may throttle
requestAnimationFrameto near-zero but theAudioWorkletkeeps running at full rate on its own thread — the density feed will simply stop updating (last value holds), which is correct, harmless behavior, not a bug: silence-adjacent ambient drone holding its last density is exactly what should happen when nobody's looking at the field driving it.
7. Reproducibility ("same seed, same output")
Two independent sources of nondeterminism must be pinned:
-
RNG — do not use
Math.random()inside the worklet; use a seeded PRNG (xorshift32 above, seed hardcoded or derived deterministically from a page-level seed constant, not fromDate.now()). -
Time epoch —
t0must be the same logical clock the shader'su_timeuses. If the shader zeroesu_timeat module load and the AudioWorklet zeroest0at the moment the user's gesture firesstartOneBit()(which could be seconds or minutes after page load), the twosin(t·1.55)curves will be in different phase even though both are individually correct sin⁴ curves — visually and sonically each looks right in isolation, but they will not agree, which fails "same seed, same output" as a system, not as individual components. Fix: capture a singlepageT0 = performance.now()once, at module init, before either system starts, and derive both the shader's time uniform and the worklet'st0from(performance.now() - pageT0) / 1000at whatever moment each one starts.
Given the fixed carrier frequency, fixed seed, and shared epoch, the bitstream is bit-for-bit reproducible on a given browser/sample-rate — note "given sample rate": delta-sigma output is not reproducible across different AudioContext.sampleRate values (44.1kHz vs 48kHz devices will produce different quantization decisions at the same wall-clock time, because the comparator fires at a different rate). This is an inherent property of the technique, not a bug to fix — state it in the artifact's evidence layer rather than pretend byte-identical cross-device reproducibility exists.
8. Headless test strategy (Playwright, no speakers)
Playwright + Chromium's audio backend runs fully in the sandboxed test environment; there is no need for a real output device to verify correctness. Concrete checks, roughly in order of value:
8.1 State machine correctness (cheap, high value)
await page.click('#sound-toggle');
await page.waitForFunction(() => window.__audioCtx?.state === 'running');
expect(await page.textContent('#sound-toggle')).toMatch(/LIVE/);
await page.click('#sound-toggle');
await page.waitForFunction(() => window.__audioCtx === null || window.__audioCtx.state === 'closed');
expect(await page.textContent('#sound-toggle')).toMatch(/OFF/);
Expose window.__audioCtx = ctx (or a getter) purely for test introspection — harmless, and standard practice.
8.2 No autoplay before gesture (regression guard for the hardest law here)
await page.goto(url);
await page.waitForTimeout(1500); // let any renderer settle
const ctxExists = await page.evaluate(() => !!window.__audioCtx);
expect(ctxExists).toBe(false); // MUST be false — nothing should have started an AudioContext yet
Also assert no AudioContext was constructed via a page.on('console')/CDP check if you want to catch the more subtle "constructed early, resumed late" pattern (constructing early is technically permitted by most autoplay policies, but constructing it before the artifact's own visible-state button exists reads as intent to autoplay — flag it in review even if the browser allows it).
8.3 Analyser-based waveform verification (proves the signal is really quantized, not just "some sound")
const stats = await page.evaluate(async () => {
const analyser = window.__audioCtx.createAnalyser();
analyser.fftSize = 2048;
window.__gainNode.connect(analyser); // tap post-gain, pre-destination
await new Promise(r => setTimeout(r, 200)); // let a few buffers flow
const buf = new Float32Array(analyser.fftSize);
analyser.getFloatTimeDomainData(buf);
const levels = new Set(buf.map(v => Math.round(v * 1000) / 1000));
return {
max: Math.max(...buf.map(Math.abs)),
distinctLevels: levels.size,
mean: buf.reduce((a,b)=>a+b,0) / buf.length,
};
});
expect(stats.max).toBeLessThan(0.1); // gain-staging ceiling, §5 (~0.077 worst case + margin)
expect(stats.mean).toBeCloseTo(0, 1); // DC-blocked, no persistent offset
// distinctLevels will be large in the time-domain buffer because the highpass/DC-blocker
// and the analyser's own float precision smear the raw ±1 steps — this does NOT verify
// "1-bit" at this tap point. To verify the true bitstream, add a debug tap (§8.4).
8.4 True bitstream verification (tap before the DC-blocker/filter) The most rigorous check requires reading the actual worklet output before any downstream filtering, since a BiquadFilterNode will turn a clean ±1 stream into a continuous-looking waveform (that's the filter doing its job). Add a debug-only second output or a MessageChannel from the worklet that periodically posts a raw sample block back to the main thread (gate this behind a ?debug=1 query param or similar so it never ships in the real artifact's hot path):
// inside process(), throttled (e.g. every 128 frames):
if (this.debugCounter++ % 128 === 0) {
this.port.postMessage(rawBlockBeforeFilter.slice()); // Float32Array snapshot
}
const raw = await page.evaluate(() => window.__lastRawBlock);
const levels = new Set(raw.map(v => Math.sign(v))); // should be exactly {-1, 1} (or {-1,0,1} if 'active' gating zeros)
expect([...levels].every(v => v === -1 || v === 1)).toBe(true);
This is the only check that actually proves the "1-bit" claim rather than just "it's quiet and doesn't clip." Include it — it's cheap and it's the one property this whole feature exists to demonstrate.
8.5 Density coupling test Drive the field to a known density (or stub the shader entirely and call pushDensity(0.9) / pushDensity(0.05) directly), then compare RMS or bit-flip-rate of the noise branch between the two states — should differ measurably (higher density → more frequent quantizer sign changes in the noise-only component). This validates the "pulse density follows field density" claim rather than just "some noise plays."
**8.6 Things Playwright cannot verify, be honest about them:**
- Perceptual loudness / whether -22 dBFS feels right on real hardware — that's a human listening pass, not automatable.
- Real autoplay-policy behavior varies by OS media-engagement heuristics (e.g. a user who has "interacted with media on this site before" gets a more permissive policy) — CI runs in a fresh profile every time and will always hit the strict path, which is actually the right thing to test (worst case), but don't assume CI passing means every real browser profile behaves identically.
- Audio glitches from real-world thread contention (GC pauses, other tabs) won't reproduce in a synthetic, mostly-idle CI sandbox.
9. What is likely to go wrong (ranked)
-
Phase drift between the visual sin⁴ pulse and the audio sin⁴ pulse because they're computed from two different clocks (
u_timereset on shader init vs.t0reset on gesture) — the single most likely subtle bug, and the one that breaks "same seed, same output" without breaking anything on either side individually. Fix per §7: one shared epoch. -
Reaching for
WaveShaperNodehard-clipping orbitcrusher-style downsampling and calling it "1-bit" — these produce audibly similar grit but are not delta-sigma, don't noise-shape, and alias badly at low bit counts. The brief's math must be implemented as a literal feedback loop, not approximated by a built-in node. -
Blocking
readPixelson the full-resolution field canvas to get the density scalar — will tank frame time on swiftshader especially. Must use the dedicated low-res FBO (§3). -
Autoplay policy violation from an
awaitbetween the click andresume()— the single most common Web Audio bug in the wild. Keep the gesture-to-resume()path synchronous. - Clipping from summing two post-quantization ±1 streams without collapsing to one integrator (§5's core correction) — easy to get wrong on a first pass because splitting the two signal paths for code clarity is the natural instinct, and it silently produces a louder, buzzier result that still "sounds okay" so it's easy to ship uncaught. The bitstream-verification test in §8.4 is the guard for this.
- DC offset creep on long-running sessions (this artifact plausibly stays open for minutes) — mitigate with the 20 Hz highpass (§5); test by letting the context run several minutes in a soak test and checking the analyser's mean stays near zero over time, not just at t+200ms.
-
Leaving the
AudioContextunclosed on page navigate/unload — add apagehide/visibilitychangehandler that callsctx.suspend()(notclose(), in case the user returns) so a background tab doesn't keep the audio thread hot indefinitely; this also matters for battery on real devices even though it's invisible in a headless test.
10. Where the law is genuinely in tension, stated plainly
- The canon says "essence marks the dot and live elements only... never a fill." There is no colour law violated by audio, but the visible state of the sound toggle is itself a "live element" in the doctrine's sense once active — pulsing the Departure label on the beat (§5.4) is the correct way to give it essence-adjacent liveness without ever using essence as a fill, consistent with existing button-loading-state precedent. No new pattern needed.
-
The "1-bit" claim is defensible at the pre-filter tap (§8.4) but not at the final
destinationtap after the DC-blocking highpass, because a highpass filter is inherently a continuous, multi-tap operation on a discrete-level input and its output is no longer discrete-valued. This is not a violation of the law so much as a boundary the law doesn't anticipate — physical 1-bit DACs also apply an analog reconstruction filter after the bitstream for exactly this reason (that filter is literally how a delta-sigma DAC produces continuous analog output from discrete pulses). State this in the artifact's own commentary rather than let a careful reader discover the "not really 1-bit at the speaker" fact and conclude the doctrine was faked. §16 Evidence rewards exactly this kind of self-disclosure. - Nothing here is possible to build against a system audio device that doesn't exist in the headless sandbox — Playwright/Chromium's Web Audio implementation runs on a null/virtual audio backend by design, so every test in §8 is verifying the signal, not "does it sound right through speakers." That gap is real and permanent for CI; it should be closed by one manual listening pass before ship, not automated around.