diff --git a/.labyricorn/devlog/audio-subsystem-contract-and-engine/contents.lr b/.labyricorn/devlog/audio-subsystem-contract-and-engine/contents.lr new file mode 100644 index 0000000..020d2ae --- /dev/null +++ b/.labyricorn/devlog/audio-subsystem-contract-and-engine/contents.lr @@ -0,0 +1,59 @@ +_model: devlog-entry +--- +schema_version: 1 +--- +title: Reviewing a contract before building on it +--- +date: 2026-09-05 +--- +author: Labyricorn +--- +summary: Phase 3 gives XZBT its audio subsystem: sixteen graph node types, routing and modulation, graph legality, components, and buses, with validation and expansion that never touch an AudioContext. The milestone began by finding four contradictions in the contract draft it was meant to build on. +--- +tags: Audio, Contracts, Web Audio, Determinism, Phase 3 +--- +source_commit: f07fc777f37a54d7740c147513c1c38fd48f38c2 +--- +body: + +XZBT can now describe sound. The [Phase 3a and 3b commit](https://git.labyricorn.com/Labyricorn/XZBT/commit/f07fc777f37a54d7740c147513c1c38fd48f38c2) adds the audio subsystem's complete authoring contract and the runtime that reads it. What it does not add is a sound anyone has heard, and the distinction is the point of this entry. + +## Reading the draft before extending it + +Phase 3 was split into installments: 3a for sources and control sources, 3b for processing, routing, components, and buses, 3c for lifecycle and protection. The 3a contract was drafted first. Before writing 3b on top of it, it was read against the PRD and against the shared contracts it claimed to build on. + +It did not survive the reading. Four defects were structural rather than cosmetic. + +The worst was self-contradiction about what a node *is*. Section 14.3 stated that nodes are keyed by node ID inside their graph, in the same style as parameters, state, and buses. Every one of the six documented examples then wrote an inline `id` field. Because XZBT treats any undeclared property in a behavior-bearing section as a fatal error, each of those examples would have been rejected by the very validator the section required — and the section's own acceptance trace asked for exactly those examples to validate with zero diagnostics. The contract could not have been implemented as written. + +The second defect was quieter: the draft specified nodes without ever saying where a node lives. There was no container. Nothing could be authored end to end, so none of the required traces could run against a real document. + +The third mattered for anyone reopening a cached exhibit on a different machine. The maximum useful frequency is defined as `min(24000, sampleRate x 0.45)`, and the draft made a violation a semantic validation error. But semantic validation happens at import, where there is no audio device and no sample rate to consult. Worse, a document authored on a 48 kHz machine would have become invalid on a 44.1 kHz one. + +The fourth broke a promise made in Phase 0. Seeded reproducibility depends on every procedural stream having a documented derivation key. The draft gave `sample-hold` its own stream and never said how the key was formed, so two correct implementations could disagree about what a seeded exhibit sounds like. + +Nine smaller gaps came out of the same pass: noise had no defined spectral slope, the impulse envelope had no math behind the word "exponential", nothing said what happened to a harmonic partial above Nyquist, and the diagnostics table had been duplicated and had already begun to drift between its two copies. + +## What the contract now says + +All four blockers are closed. Nodes are a keyed map and an `id` field is now explicitly an error. The audio graph object is defined as `nodes` plus `routes`, with its three embedding points named. The frequency ceiling is split into two tiers: a device-independent limit of 24000 enforced at import, and a clamp against the real device ceiling at node creation that warns rather than fails, so a cached exhibit still plays on different hardware. The sample-hold key is fixed as `|node|`. + +Section 15 then covers the rest of the authoring surface: gain, filter, compressor, waveshaper, delay, reverb, stereo pan, mixer, and resonator; the component instance node; audio routing and modulation with an explicit registry of which properties accept modulation and in what unit; twelve numbered legality rules, each bound to a specific diagnostic; authoring limits; components with their own scoped `inputs.*` reference namespace; sound definitions and recipes; and buses. + +Two decisions in that section are worth naming. Modulation is deliberately narrow: being numeric does not make a property modulatable, and the registry lists every one that is. And reproducibility draws a line through the middle of the subsystem — noise and impulse sample generation consumes no procedural stream and is explicitly outside the promise, while sample-hold, whose held values are observable decisions that can drive audible outcomes, is inside it. + +## Implementation + +Three modules. One holds the declarative node, limit, and modulation tables that every other consumer reads, so the validator, the expander, the engine, and the schema cannot disagree about what a filter's Q range is. One does validation, component expansion, and legality checking as pure functions that never open an `AudioContext` — the test suite asserts that no `AudioContext` exists while it runs. One resolves each node field exactly once from the sound instance's seeded stream, clamps frequencies to the live device ceiling, builds the Web Audio graph, and owns the runtime subsystem. + +The standalone command-line validator no longer carries its own copy of the audio rules. It calls the same module the runtime does, which is the direct lesson of finding a diagnostics table that had been duplicated and had already drifted. + +Sixty-two automated tests pass. They cover every trace the two contract sections require: each node type's minimal and invalid example, the identity model, both frequency tiers, resolve-once sampling, sample-hold reproducibility across seed, ordinal, and node key, every legality rule, component expansion and encapsulation, modulation summation, and the authoring limits at their exact boundaries. A separate test drives every node type through an `AudioContext` stand-in and confirms that every source started is stopped again on disposal. The artifact recorded for this milestone has SHA-256 digest `64d9932ed863dbae66f309504b88e19370ad2c9017e92c32bd61ea1ea39a3f17`. + +## What this milestone does not claim + +No sound has been heard from this build. The audio acceptance challenge, real clock synchronization checks, output peak and finite-sample capture, and listening observations for clicks and clipping are all open, and none of them can be satisfied by automated tests alone. + +Phase 3c is unwritten: automation precedence, the lifecycle state machine, unlock behavior, voice ceilings, and the measured master protection contract. The runtime does route every voice through an engine-owned master chain that an exhibit cannot bypass, but its ceiling is a placeholder rather than a verified contract, and the presence of a compressor establishes nothing on its own. Without the lifecycle contract, a one-shot voice is released on a fixed development timer; that is scaffolding, not the determinable ending the requirements ask for. + +The audio fixture added here is a contract exercise, not a reference exhibit. Visual rendering, cadence, events, scenarios, the generated UI, library hardening, benchmarks, and soak tests remain ahead, and the production artifact's user-observed direct-file import and full-browser restart check is still recorded as open.