# XZBT Format Specification 0.1 **XZBT format version:** 0.1 **Document revision:** 0.4 **Status:** Normative specification for shared contracts (document, types, ValueSpec, ConditionSpec, resolution, bindings, and transitions); the Audio subsystem authoring contract (Phase 3a sources/control sources, Phase 3b processing/routing/components/buses) is complete; audio automation precedence, lifecycle, and master protection (Phase 3c) and the remaining subsystem contracts are in progress **Related resources:** [PRD](../XZBT_0-1_MVP_Product_Requirements_Document.md), [decisions](XZBT_0-1_Gap_Closure_Decisions.md), [verification](XZBT_0-1_Verification_Gates.md) This document defines normative syntax and runtime semantics for XZBT 0.1 exhibits. An exhibit is a UTF-8 JSON document that configures generic procedural visual, audio, cadence, and orchestration primitives. It does not contain executable JavaScript. ## 1. Document Structure & Metadata ### 1.1 Root Structure A conforming `.xzbt` exhibit document consists of a top-level JSON object with the following properties: | Field | Type | Required | Description | | :--- | :--- | :---: | :--- | | `xzbt` | `string` | **Yes** | Must be exactly `"0.1"`. Mismatched versions fail validation immediately. | | `meta` | `object` | **Yes** | Exhibit identity and descriptive metadata. | | `runtime` | `object` | No | Initial simulation and PRNG seed configuration. | | `parameters` | `object` | No | User-tunable configuration definitions. Map of ID -> ParameterSpec. | | `state` | `object` | No | Simulation-owned mutable variables. Map of ID -> StateSpec. | | `signals` | `object` | No | Read-only runtime environmental signals. | | `ui` | `object` | No | Generated control grouping, widgets, and layout preferences. | | `components` | `object` | No | Reusable audio and visual sub-assemblies. | | `visuals` | `object` | No | Visual canvas, camera, rendering passes, and primitive systems. | | `audio` | `object` | No | Master bus, auxiliary buses, routing, and synthesizer definitions. | | `sounds` | `object` | No | One-shot sound event templates. | | `cadence` | `object` | No | Procedural pulse clocks, rhythm pools, and recurring trigger policies. | | `modulators` | `object` | No | Continuous low-frequency oscillators, noise, and sample-and-hold generators. | | `bindings` | `array` | No | Directed value-propagation links between sources and target properties. | | `events` | `object` | No | Discrete state-change and lifecycle trigger handlers. | | `scenarios` | `object` | No | Autonomous orchestrated sequences, timelines, and temporary overrides. | Strict unknown-field policy: Any unrecognized property in behavior-bearing sections (`parameters`, `state`, `visuals`, `audio`, `cadence`, `modulators`, `bindings`, `events`, `scenarios`) is a fatal validation error (`ERR_UNKNOWN_FIELD`). ### 1.2 Metadata (`meta`) Metadata provides provenance and UI presentation details. It never executes or modifies runtime logic: | Field | Type | Required | Description | | :--- | :--- | :---: | :--- | | `id` | `string` | **Yes** | Unique exhibit identifier. Pattern: `^[a-z][a-z0-9_-]*$`. Max 64 chars. | | `name` | `string` | **Yes** | Human-readable title displayed in the library and header. Max 128 chars. | | `version` | `string` | No | Exhibit semantic version string (e.g., `"1.0.0"`). | | `author` | `string` | No | Author or creator attribution string. Max 128 chars. | | `description`| `string` | No | Brief narrative description of the exhibit. Max 1024 chars. | | `license` | `string` | No | License terms (e.g., `"CC-BY-4.0"`, `"All Rights Reserved"`). | | `tags` | `array[string]` | No | Array of category or aesthetic keywords. Max 16 tags. | ### 1.3 Identifiers and Namespaces * **Identifier Syntax:** All resource IDs (exhibit ID, parameter IDs, state IDs, bus IDs, scenario IDs) must strictly match: ```text ^[a-z][a-z0-9_-]*$ ``` * **Reference Path Syntax:** Dot-delimited path referencing a target property or namespace: ```text .[.] ``` Valid canonical namespace prefixes: `parameters.*`, `state.*`, `signals.*`, `modulators.*`, `audio.buses.*`, `visuals.systems.*`. Dots are strictly forbidden within identifier names themselves. --- ## 2. Shared Type System & Coercion Rules Every value in XZBT belongs to one of the following concrete primitive types: | Type | Definition & Constraints | Serialization Example | | :--- | :--- | :--- | | `number` | IEEE 754 64-bit float. Must be finite (`Number.isFinite(v) === true`). `NaN`, `+Infinity`, and `-Infinity` are strictly forbidden and fail validation/evaluation. | `440.0`, `-0.5`, `1e3` | | `integer` | IEEE 754 64-bit float restricted to integer values (`Number.isInteger(v) === true`). | `1`, `42`, `-8` | | `boolean` | Logical truth value: `true` or `false`. | `true`, `false` | | `string` | Valid UTF-8 text string. | `"drift"`, `"sine"` | | `color` | CSS-compatible color: `#rgb`, `#rrggbb`, `#rrggbbaa`, or standard CSS color keyword. | `"#e8ad57"`, `"#11151c"` | | `enum` | String constrained to an explicitly declared set of allowed tokens. | `"triangle"` in `["sine", "triangle", "saw"]` | **Strict Coercion Ban:** The runtime performs **no implicit type coercion**. A string containing digits (e.g. `"440"`) will **not** be coerced into a number; a non-zero number will **not** be coerced into a boolean. Type mismatches produce `ERR_TYPE_MISMATCH`. --- ## 3. Parameter and State Specifications ### 3.1 Parameters (`parameters.`) Parameters represent user-configurable settings. They have documented defaults and bounds: ```json { "activity": { "type": "number", "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "label": "Activity Level" } } ``` * **Storage Distinction:** User edits to parameters are persisted in client storage separately from default exhibit definitions. * **Protection from `set`:** Actions triggered by scenarios or events **cannot** use `set` to mutate parameters. Temporary programmatic modifications must use `override`. ### 3.2 State Variables (`state.`) State variables represent simulation-owned state. They are initialized at launch and manipulated by runtime events and scenarios: ```json { "machine-load": { "type": "number", "initial": 0.25, "min": 0.0, "max": 1.0 } } ``` * **Persistence Distinction:** State variables are transient simulation variables and are not stored across browser restarts unless explicitly configured. --- ## 4. ValueSpec 0.1 Normative Specification A `ValueSpec` is the universal declarative expression used wherever a dynamic or configurable value is accepted. A `ValueSpec` must match one of the following five forms: ### 4.1 Literal Constant A raw JSON number, boolean, string, or color: ```json 440.0 ``` ### 4.2 Reference (`ref`) Reads the currently resolved value of a declared target: ```json { "ref": "parameters.activity" } ``` References are evaluated dynamically during each simulation tick unless used within a construct with documented static sampling timing. ### 4.3 Random Range (`random`) Samples a pseudo-random value from a uniform bounded range: ```json { "random": { "min": 200.0, "max": 800.0, "integer": false } } ``` * `min` (`number`, required): Lower bound. * `max` (`number`, required): Upper bound (`max >= min`). * `integer` (`boolean`, optional, default `false`): If `true`, output is rounded to an integer via `Math.floor(min + prng() * (max - min + 1))`. * **Sampling Boundary:** Sampled **only** at object instantiation or event invocation. It is **never** sampled per-frame. ### 4.4 Weighted Selection (`choose`) Selects one item from an array of weighted options: ```json { "choose": [ { "value": "sine", "weight": 6 }, { "value": "triangle", "weight": 3 }, { "value": "square", "weight": 1 } ] } ``` * `choose` (`array[object]`, required): Non-empty array of choice objects. * Each entry requires `value` (`literal` or nested `ValueSpec`) and `weight` (`number > 0`). ### 4.5 Calculation Operator (`op`) Evaluates an arithmetic or mathematical operation over argument operands: ```json { "op": "multiply", "args": [ { "ref": "parameters.activity" }, 1.5 ] } ``` #### Supported Operators and Arity | Operator | Arity | Description | Domain Rules & Safe Fallback | | :--- | :---: | :--- | :--- | | `abs` | 1 | Absolute value: `\|a\|` | Finite number. | | `negate` | 1 | Arithmetic negation: `-a` | Finite number. | | `round` | 1 | Nearest integer: `Math.round(a)` | Finite number. | | `floor` | 1 | Floor integer: `Math.floor(a)` | Finite number. | | `ceil` | 1 | Ceiling integer: `Math.ceil(a)` | Finite number. | | `add` | 2 | Addition: `a + b` | Finite number. | | `subtract` | 2 | Subtraction: `a - b` | Finite number. | | `multiply` | 2 | Multiplication: `a * b` | Finite number. | | `divide` | 2 | Division: `a / b` | **Division-by-zero protection:** If `b === 0`, evaluates safely to `0.0` (never `NaN` or `Infinity`). | | `min` | 2 | Minimum: `Math.min(a, b)` | Finite numbers. | | `max` | 2 | Maximum: `Math.max(a, b)` | Finite numbers. | | `clamp` | 3 | Range clamp: `[val, min, max]` | Evaluates to `Math.min(max, Math.max(min, val))`. Requires `min <= max`. | | `lerp` | 3 | Linear interpolation: `[a, b, t]` | Evaluates to `a + (b - a) * t`. Unclamped `t` unless combined with `clamp`. | --- ## 5. ConditionSpec 0.1 Normative Specification A `ConditionSpec` evaluates to a boolean (`true` or `false`) and controls scenario triggers, conditional actions, and branching logic. ### 5.1 Comparison Expressions Compares two `ValueSpec` expressions: ```json { "op": "gt", "left": { "ref": "state.machine-load" }, "right": 0.85 } ``` * Supported comparison operators: * `eq`: Equality (`left === right`) * `ne`: Inequality (`left !== right`) * `gt`: Greater than (`left > right`) * `gte`: Greater than or equal to (`left >= right`) * `lt`: Less than (`left < right`) * `lte`: Less than or equal to (`left <= right`) * `left` and `right` must evaluate to compatible primitive types (`number` with `number`, `boolean` with `boolean`, `string` with `string`). Cross-type comparison produces `ERR_TYPE_MISMATCH`. ### 5.2 Logical Combinators Combines child condition expressions: * **Conjunction (`and`):** All child conditions must evaluate to `true`. Short-circuits on first `false`. ```json { "and": [ { "op": "gt", "left": { "ref": "state.machine-load" }, "right": 0.5 }, { "op": "eq", "left": { "ref": "parameters.enabled" }, "right": true } ] } ``` * **Disjunction (`or`):** At least one child condition must evaluate to `true`. Short-circuits on first `true`. ```json { "or": [ { "op": "lt", "left": { "ref": "state.energy" }, "right": 0.1 }, { "op": "eq", "left": { "ref": "state.alarm" }, "right": true } ] } ``` * **Negation (`not`):** Inverts the child condition. ```json { "not": { "op": "eq", "left": { "ref": "parameters.mute" }, "right": true } } ``` ### 5.3 Edge-Triggering & Re-Arming Semantics * **Rising-Edge Trigger:** When a condition is used as an event trigger or scenario trigger, it fires **only** when its evaluation transitions from `false` on tick $T-1$ to `true` on tick $T$. * **Re-Arming Rule:** As long as the condition remains continuously `true`, it **will not fire again**. It must evaluate to `false` on at least one tick to re-arm before it can fire on a subsequent `true` evaluation. --- ## 6. TimeSpec and DurationSpec 0.1 ### 6.1 Duration Literals Durations are expressed as strings containing a non-negative finite number and a single explicit unit: * `ms`: Milliseconds (e.g., `"250ms"`, `"16.67ms"`) * `s`: Seconds (e.g., `"4s"`, `"0.5s"`) * `m`: Minutes (e.g., `"2.5m"`, `"1m"`) * `h`: Hours (e.g., `"1.5h"`, `"8h"`) Compound formats (e.g., `"1m30s"`) are strictly invalid (`ERR_INVALID_DURATION`). Internally, the runtime converts all durations to millisecond floating-point numbers. ### 6.2 Procedural Bounded Duration (`TimeSpec`) Where procedural timing is permitted (e.g., cadence intervals, scenario wait steps), a bounded random TimeSpec may be used: ```json { "random": { "min": "500ms", "max": "2.5s" } } ``` --- ## 7. Diagnostic Error Code Standard To ensure consistent error reporting between structural schema validation, semantic validation, and runtime execution, all diagnostic errors must use the following standard codes: | Error Code | Stage | Cause | | :--- | :--- | :--- | | `ERR_SCHEMA_VALIDATION` | Structural | Missing required fields, invalid JSON types, or malformed top-level shapes. | | `ERR_UNKNOWN_FIELD` | Structural / Semantic | Behavior-bearing block contains an undeclared property. | | `ERR_UNSUPPORTED_VERSION` | Structural | Document `xzbt` attribute is not `"0.1"`. | | `ERR_INVALID_ID` | Structural / Semantic | Identifier fails `^[a-z][a-z0-9_-]*$` regex or exceeds length limits. | | `ERR_INVALID_REFERENCE` | Semantic | Reference path does not resolve to an existing declared resource or property. | | `ERR_TYPE_MISMATCH` | Semantic / Runtime | Provided value or expression type does not match target property type. | | `ERR_CYCLIC_DEPENDENCY` | Semantic | Directed cycle detected in bindings or reference dependencies. | | `ERR_OUT_OF_BOUNDS` | Semantic / Runtime | Constant or default value violates min/max clamps. | | `ERR_INVALID_OPERATOR` | Semantic | ValueSpec `op` or ConditionSpec `op` is not in the recognized operator set. | | `ERR_INVALID_ARITY` | Semantic | ValueSpec `args` array length does not match operator requirement. | | `ERR_INVALID_DURATION` | Structural / Semantic | Duration string violates single-unit regex or contains negative values. | | `ERR_UNSUPPORTED_TARGET` | Semantic / Runtime | An action, binding, or resolution stage addresses a target that does not expose that operation. | | `ERR_CONFLICTING_BINDING` | Semantic | More than one enabled ordinary binding writes a scalar target. | | `ERR_INVALID_TRANSITION` | Semantic / Runtime | A transition is incompatible with its target, easing, scope, or duration. | | `ERR_DISPATCH_BUDGET` | Runtime | Ordinary event/action work exceeded the per-tick dispatch budget. | | `WARN_CLOCK_STALL` | Runtime | Elapsed wall time or accumulated work exceeded the fixed-step per-turn limits and was discarded. | | `WARN_CLEANUP_FORCED` | Runtime | A cleanup owner reached its deadline and force-disposed remaining resources. | | `INFO_AUDIO_UNLOCK_SKIP` | Runtime | One or more pre-unlock one-shots were intentionally not replayed. | | `ERR_INVALID_NODE_TYPE` | Semantic | Audio graph node `type` is not a member of Audio Graph Node Set 0.1. | | `ERR_NODE_LIMIT_EXCEEDED` | Semantic | An authoring limit is exceeded (oscillator partials, resonator modes, expanded nodes or routes per sound). | | `ERR_INVALID_RANGE_ORDER` | Semantic | Declared paired bounds (e.g. `sample-hold` `min`/`max`) are not in strictly increasing order after resolution. | | `ERR_INVALID_ROUTE` | Semantic | An audio or modulation route is structurally resolvable but illegal under the audio graph legality rules. | | `ERR_NO_AUDIBLE_PATH` | Semantic | A sound's expanded audio graph has no chain of audio routes from a non-control source to `output`. | | `ERR_COMPONENT_RECURSION` | Semantic | An audio component instantiates itself transitively, or component nesting exceeds 8 levels. | | `WARN_AUDIO_RATE_CLAMP` | Runtime | A frequency field was clamped to the device's `audioMaxFrequency` at node instantiation. | ## 8. Shared value resolution, bindings, and transitions ### 8.1 Resolution pipeline and target capabilities For each supported target, evaluate: ```text base -> binding -> automation -> winning override -> modulation -> safety clamp ``` Stages that the target does not expose are absent, not identity hooks available to authors. The shared 0.1 registry is: | Target family | Type and base | Binding target | Automation | Override | Additive modulation | Safety clamp | | --- | --- | :---: | :---: | :---: | :---: | --- | | `parameters.` | Declared primitive; stored user value | Yes | No | Yes | No | Declared `min`/`max`; integer targets round to the nearest integer after interpolation and before clamping | | `state.` | Declared primitive; current state value | Yes | No | Yes | No | Declared `min`/`max` | | `audio.buses..gain` | Number; bus `gain` ValueSpec | Yes | Yes | Yes | Yes | `[0, 4]` in 0.1 before master output protection | `signals.*` and `modulators.*` are read-only sources and never binding or action targets. `instances.*` is a runtime action-addressing namespace, not a ValueSpec or binding namespace. Sound inputs are sampled at sound invocation and cannot be ordinary binding targets. Visual properties, audio recipe-instance properties, and any target family absent from this table remain unsupported until their subsystem contract adds an explicit capability row. Merely being numeric does not grant automation, override, or modulation support. Parameters store user configuration separately from exhibit defaults. State stores simulation values separately from parameters. State is not an implicit layer above user configuration. References read the value resolved for the current logical tick. Parameter controls read and edit stored user values and show an override indicator whenever any live override exists for the parameter, including a masked or releasing override. Within one logical tick, resolve the directed dependency graph in topological order. Each source reference observes the source's resolved value for that same tick. Dependency cycles are `ERR_CYCLIC_DEPENDENCY`; the runtime must not insert an undocumented previous-tick delay. ### 8.2 BindingSpec The canonical fields are `source`, `target`, `scale`, `offset`, `clamp`, `smoothing`, and `when`. Earlier planning-only spellings `from`, `to`, and `transform` are not 0.1 aliases and are rejected as `ERR_UNKNOWN_FIELD`. ```json { "source": "state.machine-load", "target": "audio.buses.deep.gain", "scale": 0.5, "offset": 0.5, "clamp": [0, 1], "smoothing": "50ms", "when": { "op": "gt", "left": { "ref": "parameters.activity" }, "right": 0 } } ``` `source` and `target` are required reference paths. `scale` and `offset` are finite numbers with defaults `1` and `0`. `clamp`, when present, is a two-number inclusive `[minimum, maximum]` array with `minimum <= maximum`. `smoothing` is a DurationSpec and defaults to `"0ms"` (disabled). `when` is a ConditionSpec and defaults to `true`. For numeric sources and targets, first calculate `clamp(source * scale + offset)`. A nonnumeric binding must omit `scale`, `offset`, `clamp`, and nonzero `smoothing`, and its source and target types must match exactly. A false `when` disables the binding for that tick and exposes the target's base stage. When a binding becomes enabled, its smoother initializes to the transformed source value; disabled time is not replayed. Nonzero smoothing is the deterministic one-pole update below, evaluated once per fixed logical tick. `tau` is the authored smoothing duration in seconds, `dt` is the fixed logical step, `x` is the transformed and binding-clamped input, and `yPrevious` is the prior enabled tick's binding output: ```text alpha = 1 - exp(-dt / tau) y = yPrevious + alpha * (x - yPrevious) ``` At most one ordinary binding may target a scalar property. Multiple writers are `ERR_CONFLICTING_BINDING` even when their `when` conditions appear mutually exclusive; conditional exclusivity is not a precedence mechanism. A binding to a missing or read-only target is `ERR_UNSUPPORTED_TARGET`. ### 8.3 Override action and precedence An override action has required `type: "override"`, `target`, `value`, and `scope` fields. It may also contain common action fields plus `priority`, `duration`, and `transition`. | Field | Contract | | --- | --- | | `target` | A target whose capability row permits override. | | `value` | ValueSpec sampled once when the override instance activates; result must match the target type. | | `scope` | `"scenario"` or `"duration"`. | | `priority` | Optional integer from `-1000` through `1000`. If omitted under a scenario, inherit the scenario instance priority; otherwise use `0`. | | `duration` | Required and greater than `0ms` for duration scope; forbidden for scenario scope. Time begins at activation, including while masked. | | `transition` | Optional OverrideTransitionSpec; defaults to zero-duration `in` and `out` with `linear` easing. | Each activated override receives a monotonically increasing runtime `activationSequence` within the performance and a runtime ID `instances.override-`. An author `id`, when supplied, identifies the action definition and does not replace the runtime ID. The live winner is the greatest tuple `(priority, activationSequence)`. Scope affects lifetime only: duration scope receives no precedence advantage over scenario scope. Underlying binding and automation stages continue to evaluate while masked. Overrides also advance their own durations and envelopes while masked. When a winner changes, the newly winning override's attack begins from the target's current pre-modulation resolved value, preventing a discontinuity. If that override had already completed its attack while masked, it takes effect immediately at its sampled value. ### 8.4 TransitionSpec and interruption A numeric `set` action accepts `{ "duration": DurationSpec, "easing": Easing }`, with defaults `"0ms"` and `"linear"`. An override accepts `{ "in": DurationSpec, "out": DurationSpec, "easing": Easing }`, with both durations defaulting to `"0ms"`. Easing is one of `linear`, `ease-in`, `ease-out`, or `ease-in-out`, defined for normalized `t` in `[0,1]`: ```text linear: t ease-in: t * t ease-out: 1 - (1 - t) * (1 - t) ease-in-out: 2 * t * t when t < 0.5 1 - ((-2 * t + 2)^2) / 2 otherwise ``` Zero duration is an immediate step. Non-numeric targets permit only zero-duration transitions; any nonzero duration is `ERR_INVALID_TRANSITION`. Numeric interpolation uses double precision. Integer targets round half away from zero after interpolation and then apply their declared clamp. An attack interpolates from the pre-modulation value visible immediately before the override becomes winner to its sampled override value. Release begins when duration expires, its scenario owner terminates, or the override is explicitly removed. Release output on each tick is: ```text lerp(currentLowerValue, releaseStartValue, 1 - easing(elapsed / outDuration)) ``` `currentLowerValue` is recomputed on every tick without the releasing override, so stored user edits, bindings, and automation remain observable during release. `releaseStartValue` is the override's actual pre-modulation output when release began. A zero-duration release removes the override before that tick resolves. A higher-priority interruption does not cancel a lower override. The lower override continues its lifetime and release while masked. If the higher override releases, the next live winner is selected and its current envelope value is used. Reactivating the same action creates a distinct override instance with a new activation sequence. The safety clamp always runs after modulation. A constant override or binding value outside the target's legal range is `ERR_OUT_OF_BOUNDS` at validation; a dynamic result is clamped at runtime and emits a rate-limited `ERR_OUT_OF_BOUNDS` diagnostic rather than poisoning the graph. ### 8.5 Required resolution traces The normative deterministic traces are stored with the GC3 tests. They cover: | Case | Expected behavior | | --- | --- | | Bus gain is bound to activity, then directly overridden | Override supplies the pre-modulation value; the binding continues underneath and its current value becomes the release destination | | Activity is overridden and referenced by a binding | The binding observes resolved activity during the same logical tick | | User edits stored activity while its override is active | Stored edit persists; release approaches the updated lower value | | Two overrides compete | Higher priority wins; equal priority uses greater activation sequence | | Duration and scenario overrides compete | Priority and activation sequence decide, not scope | | A target has legal additive modulation | Modulation follows the winning override; safety clamp runs last | | Binding conditions, smoothing, clamps, and invalid graphs | Disabled binding exposes base, re-enable initializes smoothing, and conflicts/cycles are rejected | ## 9. Time, audio alignment, and random evaluation ### 9.1 Fixed logical clock The 0.1 logical step is exactly `1000 / 60` milliseconds. Runtime time is represented by an integer `tickIndex`; logical seconds are `tickIndex / 60`. Rendering reads the most recently published state and never advances simulation time. The foreground driver accumulates nonnegative elapsed monotonic time. One browser turn executes at most eight logical ticks. A single driver observation contributes at most `250ms`; excess elapsed time is discarded and emits one rate-limited `WARN_CLOCK_STALL` diagnostic. After eight ticks, the driver yields and retains at most one step of accumulator; additional accumulated time is discarded with the same diagnostic. These bounds prevent catch-up bursts. They are runtime responsiveness policy, not permission for accelerated tests to skip authored ticks. Each logical tick uses this order: 1. Increment `tickIndex` and latch queued user/external inputs in arrival-sequence order. Tests replace external inputs, including audio analysis, with deterministic fixtures. 2. Advance time-based modulators and automation; resolve the dependency graph in topological order. 3. Evaluate cadence, event conditions, and scenario timelines once against that tick's resolved snapshot; enqueue work using document order as the final tie-break. 4. Drain ordinary event/action work up to the GC5 dispatch budget. Actions in one array execute in document order; a state mutation invalidates affected resolved values before the next action executes. 5. Advance owned audio/visual instance state and perform mandatory cleanup, which is not charged to the ordinary dispatch budget. 6. Schedule eligible audio work within the lookahead horizon, then publish the immutable state read by rendering and generated UI. Application pause and document visibility are independent pause reasons. Logical progression and audio are paused while either reason is present. On any transition from paused to running, reset the monotonic wall-time anchor while preserving `tickIndex` and the sub-step accumulator. Do not add paused wall time or replay missed work. Removing the visibility reason does not remove an explicit application pause. ### 9.2 Audio clock mapping and unlock The initial audio scheduling lookahead is `100ms`, refilled after every logical tick. Maintain an anchor pair `(logicalSeconds, audioContext.currentTime)` and map an eligible logical start to the audio clock relative to that pair. On audio unlock or resume, establish a fresh anchor at the current logical position. Engine-owned future starts from an obsolete anchor are cancelled and rescheduled where the Web Audio node contract permits it. If audio is locked, logical sound invocations still receive their deterministic IDs, ownership, sampled values, and nominal logical intervals, but create no Web Audio nodes. At unlock: - a one-shot whose logical start is earlier than the unlock position is skipped, whether expired or partially elapsed; emit one rate-limited `INFO_AUDIO_UNLOCK_SKIP` count rather than replaying it; - a continuous sound that is still logically owned starts at its current logical phase/state, not from its original attack, and applies an engine-owned `20ms` anti-click ramp from silence to its current gain; - future invocations inside the new lookahead are scheduled normally. Pause or visibility loss suspends the AudioContext after cancelling engine-owned future starts. Resume re-anchors and refills the horizon. Accelerated logical tests validate scheduling decisions only; audible behavior, anti-click ramps, and real-time soak acceptance require browser/audio observations. ### 9.3 Seed normalization and PRNG An authored numeric seed must be an integer in `[0, 4294967295]` and is normalized with unsigned 32-bit semantics. For `"random"`, obtain one unsigned 32-bit integer from `crypto.getRandomValues`, store it with the performance record, and use that recorded integer for every subsequent derivation. Failure to obtain entropy is a startup error; time or `Math.random()` is not a fallback. XZBT 0.1 uses `xoshiro128**` with unsigned 32-bit arithmetic. A stream seed is FNV-1a-32 over the UTF-8 bytes of: ```text xzbt-0.1\0\0\0 ``` Expand that hash into four words with SplitMix32. If all four words are zero, set the fourth word to `1`. Each sample converts the next unsigned result to `[0,1)` by division by `4294967296`. Integer ranges use unbiased rejection sampling; weighted choices consume one sample and select by cumulative positive weight in document order. The reserved domains are `cadence`, `scenario`, `visual`, `sound`, and `manual-sample`. Stable instance keys use the declared definition ID plus that definition's monotonically increasing invocation ordinal within its domain. A subsystem may add a documented child key but may not draw from another domain. Rendering consumes no procedural stream. Manual SAMPLE always uses `manual-sample`; therefore it cannot perturb cadence, scenario, visual, or automatic sound choices. Random and weighted-choice ValueSpecs are sampled once at the containing object's documented instantiation or invocation boundary. Nested random ValueSpecs use the same owning stream in depth-first, property-document order. Evolving randomness belongs to modulators and consumes only the owning modulator/visual stream at logical ticks. Reproducibility means identical procedural decisions for the same XZBT runtime version, normalized seed, stable exhibit definition, and logical input sequence. It does not promise identical pixels, floating-point audio samples, or browser timing across devices. ## 10. Ownership, failure, and bounded dispatch ### 10.1 Ownership propagation Every action dispatch carries an ownership context: a scenario instance owner or the performance root. An event action passes the same context to the invoked event; nesting never resets ownership. Sound, spawn, subscription, and override resources inherit that context unless their resource contract permits `ownership: "persistent"` and the action explicitly requests it. Persistent resources transfer to the performance root. Unsupported persistent ownership is `ERR_UNSUPPORTED_TARGET`. A scenario instance owns its future timeline records, repeats, relative actions, subscriptions, scenario-scope overrides, duration overrides it creates, and nonpersistent continuous audio/visual resources, including resources created indirectly through nested events. A duration override owned by a scenario begins release at its authored deadline or owner termination, whichever occurs first. `set` changes are persistent mutations, not owned resources. A successfully executed `set` is never rolled back because a later action, hook, or scenario fails. ### 10.2 Hook order and allowed termination work `onStart` runs after the instance owner and deterministic streams exist but before timeline activation. It may use the full Action Model. A critical `onStart` failure cancels remaining start actions, prevents timeline activation, and terminates the instance as `FAILED`. Termination first blocks new ordinary dispatch for the owner and cancels its future timeline/repeat/relative work. Exactly one termination hook then runs: | Cause | Hook | Terminal state after cleanup | | --- | --- | --- | | Natural duration/timeline completion | `onComplete` | `COMPLETED` | | Manual cancel, replacement, exhibit deactivation | `onCancel` | `CANCELLED` | | Critical startup, ordinary action, or nested-event failure | `onCancel` | `FAILED` | Termination hooks execute their actions in document order and may contain only `set` and one-shot `sound` actions. Termination sounds are owned by the cleanup owner and may not request persistent ownership. `override`, `event`, `spawn`, `remove`, and `control` actions in a termination hook are validation errors. A hook action failure is diagnosed and remaining hook actions continue regardless of `critical`; it cannot suppress cleanup or replace the original terminal cause. After the hook attempt, detach subscriptions, release overrides and continuous audio, remove visual systems, clear scheduler records, and release runtime references. Release work transfers to an engine cleanup owner. Each release uses the shorter of its authored release and `5s`; the cleanup owner has an absolute deadline of five seconds of advancing logical time, after which remaining resources are force-disposed with `WARN_CLEANUP_FORCED`. Destroying or replacing the entire performance force-disposes immediately after its deactivation hook; it does not leave a detached timer. ### 10.3 Condition trigger state A condition trigger has `disarmed`, `holding`, and `armed` states. A false evaluation clears its hold timer and arms it. Once armed, continuous true evaluations accumulate logical ticks; it fires once when the declared `for` interval is met, then becomes disarmed. It cannot fire again until at least one later logical tick evaluates false. Startup with a true condition begins disarmed, preventing an unobserved pre-launch edge from firing. ### 10.4 Deferred starts Each scenario definition may have at most one deferred request. Its creation tick and sampled trigger inputs are retained. A later opportunity for the same definition neither adds a request nor extends expiry. The expiry is `eligibility.timeout` when present, otherwise `5m`, measured in advancing logical time. At each tick, remove expired requests first. Then consider the remaining queue in descending scenario priority, ascending creation tick, and scenario document order. Immediately before dispatch, recheck that the definition is enabled, its eligibility is true, cooldown permits it, and its concurrency conflict has cleared. A failed recheck leaves the request pending until a later tick or expiry; it does not resample the original trigger. ### 10.5 Dispatch budget and feedback Static validation rejects direct and indirect event invocation cycles and detectable event/scenario trigger feedback cycles as `ERR_CYCLIC_DEPENDENCY`. Runtime uses a second backstop because data-dependent feedback may remain. One logical tick permits at most `1024` ordinary dispatch units. Entering an event consumes one unit and each attempted action consumes one unit, including skipped `when`/`chance` actions. When the next unit would exceed the budget, emit `ERR_DISPATCH_BUDGET`, discard the remaining ordinary queue for that tick, and terminate each scenario owner represented by discarded or currently executing feedback work as `FAILED`. Unowned work is dropped. Mandatory termination hooks and cleanup are budget-exempt and use a separate hard limit of `256` termination-hook actions per owner; exceeding it truncates the hook and continues cleanup. The maximum nested event depth remains 16 and is checked before entering the next event. Budget and depth failures are critical runtime failures for an owned chain. Diagnostics include tick, owner, event/action path, consumed units, and discarded queue count. ## 11. Contract completion register Complete shared contracts before implementing dependent subsystems. Use PRD section numbers as stable lookup references. | Contract | Existing PRD input | Required completion | Status | | --- | --- | --- | --- | | Document/schema | 9-14, 113-116, 121 | Top-level shapes, unknown-field policy, identifier regex, reference paths, standard diagnostic codes | **Complete (Rev 0.2)** | | Values and conditions | 15-21, 33 | Operator arity/table, division-by-zero protection, sampling timing boundaries, edge-trigger re-arming | **Complete (Rev 0.2)** | | References and bindings | 13, 17, 31-32 | Target-capability table, instance/input scope, evaluation order, cycles, disabled bindings, exact smoothing | **Complete (Rev 0.3 / GC3)** | | Actions and transitions | 22-30 | Shared `set`/`override` fields and defaults, override target matrix, interrupted transitions, instance IDs; subsystem action matrices remain with their subsystems | **Shared contract complete (Rev 0.3 / GC3)** | | Audio | 34-60 | Complete recipe/component shapes, node defaults, automation timing, clamp implementation, one-shot endings and tails, unlock behavior, master protection contract | **Authoring contract complete (Rev 0.4 / Phase 3a-3b):** units, graph objects, all sixteen node types, routing, modulation, graph legality, authoring limits, components, sounds/recipes, and buses. Automation precedence (54), lifecycle states (57), unlock behavior, and runtime safety/master protection (58) remain (Phase 3c) | | Cadence | 61-68 | Exact selection/cooldown/overlap fields, clocks, pool collisions, pending audio, manual sampling isolation and continuous-sample termination | Subsystem contract (Phase 5) | | Visuals | 69-89 | Primitive/system/behavior fields, angle and motion units, transform order, depth projection, field behavior, morph compatibility, effect approximation and limits | Subsystem contract (Phase 4) | | Events/scenarios | 90-102 | Shared ownership, hooks/failure ordering, condition rearming, deferred ordering/expiry, dispatch limits; full trigger/timeline shapes remain for Phase 6 | **Shared lifecycle contract complete (Rev 0.3 / GC5)** | | Generated UI | 103-107 | Widget compatibility, button actions, parameter validation and override display, group/control ordering | Subsystem contract (Phase 7) | | Runtime/library | 108-112, 117-127 | Clock/audio synchronization, stalls, seed/stream derivation complete in GC4; import equality, update compatibility, transactions, failure recovery, persistence schema remain | **Shared clock/PRNG contract complete (Rev 0.3 / GC4); subsystem remainder Phase 1/8** | ## 12. Contract template and conformance artifacts Each construct must record its JSON shape; required and optional fields; types, units, ranges, and defaults; supported ValueSpec fields and evaluation timing; read/write namespaces; lifecycle and ownership; precedence; validation errors; runtime failure behavior; and resource costs or limits. Supply a valid minimal example, a meaningful composition example, invalid cases with expected diagnostics, and expected semantic traces where timing or ordering matters. Two contrasting complete exhibits must exercise parameters, sound, visuals, and a temporary scenario override early in development. ## 13. Library identity, update compatibility, and build boundary ### 13.1 Import identity After successful UTF-8 decoding, remove one leading byte-order mark if present. The resulting source bytes are the import payload. Compute SHA-256 over those bytes; equality requires both the same `meta.id` and the same digest. An identical import is a no-op and does not change cached timestamps, source associations, selected exhibit, or parameter values. The same ID with a different digest is a replacement candidate even if parsed values appear equivalent. Whitespace, property-order, or numeric-spelling changes may therefore require confirmation; this conservative rule preserves document-order semantics and exact provenance. A changed candidate is parsed and fully validated before the user is offered explicit replace/cancel choices. Invalid candidates never replace the cached valid definition. ### 13.2 Parameter reconciliation After an explicit replacement choice, reconcile stored parameters by parameter ID: | Change | Result | | --- | --- | | Same type and still valid | Preserve stored value. | | `number`/`integer` bounds changed | Clamp to the new inclusive range and show a notice; integer values are rounded half away from zero before clamping. | | Enum values changed | Preserve only if the stored token remains declared; otherwise use the new default and show a notice. | | Type changed, including `number` to `integer` | Use the new default and show an incompatibility notice. | | Parameter removed | Delete its stored value after successful activation. | | Parameter added | Initialize from the new default. | Do not mutate the old cached definition or settings while validating or preparing a candidate. Preparation may allocate parsed data and inert compiled plans but must not start timers, render visible output, create audible nodes, or persist replacement state. On successful candidate activation, atomically commit the new definition, reconciled settings, digest, and source metadata, then dispose the old performance. If activation fails after the old performance has stopped, dispose the candidate and attempt a fresh activation of the old cached definition with its unchanged saved settings. Report both failures if recovery fails; never represent the failed candidate as active. ### 13.3 Reproducible standalone build Development uses separate source modules. The build must have pinned tool versions and inputs, stable module/asset order, no wall-clock timestamps or absolute paths in output, and a documented single command that produces `XZBT.html`. Two clean builds from the same revision must have identical SHA-256 digests. The artifact must contain all runtime code/assets/fonts and pass direct-file offline verification with no external runtime requests. Structural JSON Schema does not replace semantic validation. The internal schema and fixtures belong to 0.1 implementation work; public schema distribution and editor integration may follow later. ## 14. Audio Subsystem Contract — Sources and Control Sources (Phase 3a) This section opens the Audio contract required by section 11 (PRD 34-60). It covers the audio pipeline overview, canonical units, the shared audio-graph object shape, node-field resolution scope, the audio frequency ceiling, audio reproducibility scope, and the six source and control-source node types (PRD 36-42). Processing nodes, routing and modulation, graph legality, components, sound definitions, and buses (PRD 43-53, 55, 56, 59, 60) are specified in section 15 (Phase 3b). Audio automation precedence (PRD 54), the full runtime lifecycle state machine (PRD 57), and the consolidated safety-limit and master-protection contract (PRD 58) are **Not yet specified** and arrive in Phase 3c. ### 14.1 Pipeline and scope The audio pipeline is, in order: audio primitives (14.7-14.12) → audio graphs (14.3) → reusable `components.audio.*` components (15.15) → sound recipes (15.16) → sound instances → declared buses (15.17) → bus processing → engine master protection → output (PRD 34). XZBT never implements a node, field, or function named after a specific exhibit's thematic sound. Every construct in this contract is generic. ### 14.2 Canonical units | Quantity | Unit | Notes | | --- | --- | --- | | frequency | Hz | Bounded above by `audioMaxFrequency` (14.5). | | detune | cents | `-4800` to `+4800` unless a node contract states otherwise. | | gain / amplitude | linear scalar | Not dB unless the field's contract says dB. | | filter and compressor gain | dB | Applies to `filter.gain`, `compressor.threshold`, and `compressor.knee` (15.3, 15.4). | | Q | unitless | Applies to `filter.q` (15.3). | | normalized controls | `0` to `1` | Polarity-free control ranges (impulse `amplitude`, `mix`, `damping`, waveshaper `amount`). | | pan | `-1` to `+1` | `-1` full left, `0` center, `+1` full right (15.8). | | time | DurationSpec 0.1 | Section 6; a procedural `TimeSpec` is permitted wherever a field's contract says DurationSpec. | | modulation depth | target property's unit | Section 15.13 (PRD 53). | ### 14.3 Audio graph objects An **audio graph object** is the shared container for every audio node network in an exhibit: ```json { "nodes": { "tone": { "type": "oscillator", "frequency": 220 }, "level": { "type": "gain", "gain": 0.4 } }, "routes": [ { "from": "tone", "to": "level" }, { "from": "level", "to": "output" } ] } ``` | Field | Type | Required | Notes | | --- | --- | :---: | --- | | `nodes` | object keyed by node ID | Yes | Each value is a node object (14.4). An empty `nodes` object is `ERR_SCHEMA_VALIDATION`. | | `routes` | array | No (defaults to `[]`) | Route objects, specified in 15.12-15.13. A graph object with no route reaching `output` is `ERR_NO_AUDIBLE_PATH` unless it is a component graph, which terminates at its declared component output instead (15.15). | Audio graph objects are embedded at exactly three places, whose surrounding fields are specified in Phase 3b: * `audio.recipes.` — a named, reusable recipe graph (15.16). * `sounds..recipe` — an inline recipe graph (15.16, PRD 59). * `components.audio.` — a component graph, which adds `parameters` and `input` declarations (15.15, PRD 56). **Node identity.** Nodes are addressed by their key in the `nodes` object, matching how the document keys `parameters`, `state`, `audio.buses`, `sounds`, and `modulators`. A node object therefore carries **no** `id` field; supplying one is `ERR_UNKNOWN_FIELD`. Node keys follow the shared identifier rule (section 1.3) and are unique by construction; a key failing the identifier regex is `ERR_INVALID_ID`. **Reserved key.** `output` is reserved as the graph's audible sink and may not be declared in `nodes` (`ERR_INVALID_ID`). It is sink-only: `output` may not appear as a route `from` (15.14, PRD 55). **Node object fields.** | Field | Type | Required | Notes | | --- | --- | :---: | --- | | `type` | string enum | Yes | A member of Audio Graph Node Set 0.1 (PRD 36): `oscillator`, `noise`, `impulse`, `constant`, `lfo`, `sample-hold`, `gain`, `filter`, `compressor`, `waveshaper`, `delay`, `reverb`, `stereo-pan`, `mixer`, `resonator`, or `component`. Any other value is `ERR_INVALID_NODE_TYPE`. | | *(type-specific fields)* | — | Per node | 14.7-14.12 and 15.1-15.8. | Per the strict unknown-field policy (section 1), any property on a node object that its `type` contract does not declare is `ERR_UNKNOWN_FIELD`. ### 14.4 Node-field resolution scope Numeric and enum fields on node objects are authored with ValueSpec 0.1 (section 4) and DurationSpec 0.1 (section 6) exactly where each field table says so. Every such field is resolved **once**, at the owning sound instance's instantiation boundary — the same boundary section 9.3 already fixes for `random` and `choose` sampling — and is constant for that node instance's lifetime. Nested ValueSpecs within one graph are sampled in depth-first, property-document order from the sound instance's own stream, per section 9.3. Node fields are **not** added to the section 8.1 target-capability table. A `BindingSpec`, `set` action, or `override` action addressing a node field (for example `sounds.hum.recipe.nodes.tone.frequency`) is `ERR_UNSUPPORTED_TARGET`. Section 8.1's existing `audio.buses..gain` row is unaffected and remains the supported external control surface for audio in 0.1. This restriction governs **external** targeting only. Graph-internal modulation — a route whose `to` addresses `node.property` (15.13, PRD 52-53) — is the supported mechanism for varying a node field over time and is fully specified in Phase 3b. Section 15.13 lists which properties accept modulation; a modulation route to any other property is `ERR_UNSUPPORTED_TARGET`. Automation tracks (PRD 54) are a separate Phase 3c stage and are **Not yet specified**; an `automation` field on any node or recipe is `ERR_UNKNOWN_FIELD` until that contract lands. ### 14.5 Audio frequency ceiling and validation staging ```text audioMaxFrequency = min(24000, sampleRate x 0.45) ``` `sampleRate` is the live `AudioContext.sampleRate`, which is a property of the playback device and is unknown while an exhibit is being imported or validated. The ceiling is therefore enforced in two tiers, and an implementation must not conflate them: 1. **Semantic stage (import, refresh, and validation tooling).** A frequency field whose authored value resolves to a literal outside its declared range, checked against the device-independent ceiling `24000`, is `ERR_OUT_OF_BOUNDS`. Validation never opens an `AudioContext` and never depends on audio hardware. 2. **Instantiation stage (node creation on a live context).** The runtime computes `audioMaxFrequency` from the actual `AudioContext.sampleRate` and **clamps** the resolved value into range. Clamping, not failing, is required: a cached exhibit authored on a 48 kHz device must still activate on a 44.1 kHz device. Each clamped node field raises `WARN_AUDIO_RATE_CLAMP` once per node instance. A hardcoded `24000` at instantiation is a contract violation even though it is the correct semantic-stage ceiling. ### 14.6 Reproducibility scope for audio sources Section 9.3 promises identical *procedural decisions*, not identical floating-point audio samples. This contract fixes where that line falls for audio sources: * **`noise` and `impulse` sample generation consumes no XZBT procedural stream.** Their per-sample output comes from the audio implementation's own generator and is not reproducible across runs or devices. It cannot perturb any seeded stream, because it draws from none. * **`sample-hold` does draw from a seeded stream.** Its held values are observable control decisions that can drive audible outcomes, so they are reproducible. It uses domain `sound` with the documented child key defined in 14.12. * **Every other source and control-source field** is a ValueSpec resolved once at instantiation from the sound instance's stream (14.4), and is therefore reproducible. Rendering and audio callbacks consume no procedural stream (section 9.3); a `sample-hold` tick is a logical decision, not a render step, and its draws occur on the node instance's own stream in tick order. ### 14.7 Oscillator (`oscillator`) ```json { "type": "oscillator", "waveform": "sine", "frequency": 440, "detune": 0 } ``` | Field | Type | Range | Default | ValueSpec | | --- | --- | --- | --- | --- | | `waveform` | enum | `sine`, `triangle`, `square`, `sawtooth`, `custom` | `sine` | No — literal enum only. | | `frequency` | ValueSpec\ | `0.1` Hz to `audioMaxFrequency` (14.5) | `440` | Yes (14.4 scope). | | `detune` | ValueSpec\ | `-4800` to `+4800` cents | `0` | Yes (14.4 scope). | | `harmonics` | array | `1` to `64` entries | — | Required if and only if `waveform` is `custom`. | An `oscillator` is an audible source and may also serve as an audio-rate modulation source (PRD 41). **Custom partials.** Each `harmonics` entry is `{ "ratio": number, "gain": number, "phase": number }`: | Entry field | Range | Default | Meaning | | --- | --- | --- | --- | | `ratio` | `0.001` to `256` | required | Partial frequency as a multiple of the node's resolved `frequency`. | | `gain` | `0` to `1` | required | Partial linear amplitude relative to the fundamental. | | `phase` | `0` to `360` (degrees, `360` excluded) | `0` | Partial starting phase offset. | More than 64 entries is `ERR_NODE_LIMIT_EXCEEDED`. `harmonics` present while `waveform` is not `custom` is `ERR_UNKNOWN_FIELD`. `harmonics` absent while `waveform` is `custom` is `ERR_SCHEMA_VALIDATION`. An empty `harmonics` array is `ERR_SCHEMA_VALIDATION`. Entry values outside their range are `ERR_OUT_OF_BOUNDS`. At instantiation, any partial whose `ratio x frequency` exceeds `audioMaxFrequency` is **omitted** from the realized waveform rather than aliased or clamped. Omission is a routine consequence of legal authoring and raises no diagnostic; it is not the `WARN_AUDIO_RATE_CLAMP` case of 14.5, which applies to the node's own `frequency`. The starting phase of a non-`custom` oscillator is runtime-defined and is not part of this contract; exhibits must not depend on the phase relationship between two independently created oscillators. **Minimal example:** `{ "type": "oscillator" }`. **Composition example:** `{ "type": "oscillator", "waveform": "custom", "frequency": 220, "harmonics": [{ "ratio": 1, "gain": 1 }, { "ratio": 2.7, "gain": 0.35, "phase": 90 }] }`. **Invalid case:** `{ "type": "oscillator", "waveform": "sine", "harmonics": [] }` → `ERR_UNKNOWN_FIELD` on `harmonics`. ### 14.8 Noise (`noise`) ```json { "type": "noise", "color": "pink" } ``` | Field | Type | Range | Default | ValueSpec | | --- | --- | --- | --- | --- | | `color` | enum | `white`, `pink`, `brown` | `white` | No — literal enum only; `color` selects a fixed spectral shape, not a continuously varying parameter. | Spectral shapes, measured over `20` Hz to `20000` Hz: | `color` | Power spectral slope | Normalization | | --- | --- | --- | | `white` | `0` dB/octave (flat) | Unit RMS | | `pink` | `-3` dB/octave | Unit RMS | | `brown` | `-6` dB/octave | Unit RMS | The filter used to realize `pink` and `brown` is runtime-defined; its response must track the declared slope within `±3` dB across `20` Hz to `20000` Hz. All three colors are normalized to equal RMS so that changing `color` does not change perceived level. A `noise` node has no built-in volume control; author a downstream `gain` node (15.2) for level. **Minimal example:** `{ "type": "noise" }`. **Invalid case:** `{ "type": "noise", "color": "grey" }` → `ERR_TYPE_MISMATCH`. ### 14.9 Impulse (`impulse`) ```json { "type": "impulse", "color": "white", "duration": "10ms", "amplitude": 1, "decay": "exponential" } ``` | Field | Type | Range | Default | ValueSpec | | --- | --- | --- | --- | --- | | `color` | enum | `white`, `pink`, `brown` | `white` | No. Spectral shapes are those of 14.8. | | `duration` | DurationSpec 0.1 | `1ms` to `500ms` | `10ms` | Sampled once at instantiation (14.4). | | `amplitude` | ValueSpec\ | `0` to `1` | `1` | Yes (14.4 scope). | | `decay` | enum | `flat`, `linear`, `exponential` | `exponential` | No. | An impulse is inherently one-shot: it emits a single finite burst of `duration` and then produces silence. Given resolved amplitude `a`, resolved duration `d`, and normalized progress `p = t / d` over `0 <= p < 1`, the envelope applied to the `color` source is: | `decay` | Envelope | Value at `p = 1` | | --- | --- | --- | | `flat` | `a` | `0` | | `linear` | `a x (1 - p)` | `0` | | `exponential` | `a x e^(-6.907755 x p)` (`-60` dB at `p = 1`) | `0` | The envelope is exactly `0` for `p >= 1`. Because `flat` and `exponential` do not reach zero on their own, the runtime applies a terminal linear fade to zero over the final `min(1ms, d x 0.1)` of the burst; this fade is part of the contract, not an optional anti-click measure. A `duration` that resolves outside `[1ms, 500ms]` is `ERR_OUT_OF_BOUNDS`. **Minimal example:** `{ "type": "impulse" }`. **Invalid case:** `{ "type": "impulse", "duration": "1s" }` → `ERR_OUT_OF_BOUNDS`. ### 14.10 Constant (`constant`) ```json { "type": "constant", "value": 1 } ``` Control-only source (PRD 40). A `constant` may modulate numeric properties through a modulation route (15.13) but may never reach `output`, directly or through any chain; such a route is `ERR_INVALID_ROUTE` under graph legality (15.14). | Field | Type | Range | Default | ValueSpec | | --- | --- | --- | --- | --- | | `value` | ValueSpec\ | `-1000` to `1000` | `1` | Yes (14.4 scope). | **Minimal example:** `{ "type": "constant" }`. **Invalid case:** `{ "type": "constant", "value": 5000 }` → `ERR_OUT_OF_BOUNDS`. ### 14.11 LFO (`lfo`) ```json { "type": "lfo", "waveform": "sine", "frequency": 1, "amplitude": 1, "polarity": "bipolar", "phase": 0 } ``` Control-only source (PRD 41). Audio-rate modulation uses an ordinary `oscillator` instead. An `lfo` reaching `output` through any chain is `ERR_INVALID_ROUTE` (15.14). | Field | Type | Range | Default | ValueSpec | | --- | --- | --- | --- | --- | | `waveform` | enum | `sine`, `triangle`, `square`, `sawtooth` | `sine` | No. `custom` is oscillator-only and is `ERR_TYPE_MISMATCH` here. | | `frequency` | ValueSpec\ | `0.001` to `40` Hz | `1` | Yes (14.4 scope). | | `amplitude` | ValueSpec\ | `0` to `1000` | `1` | Yes (14.4 scope). Matches the `constant` output range; a modulation route's `depth` scales this into the target's unit (15.13). | | `polarity` | enum | `bipolar`, `unipolar` | `bipolar` | No. `bipolar` spans `[-amplitude, +amplitude]`; `unipolar` spans `[0, amplitude]`. | | `phase` | ValueSpec\ | `0` to `360` (degrees, `360` excluded) | `0` | Yes (14.4 scope). | **Phase origin.** An `lfo` phase is measured from the node instance's own start, not from a global transport: at the instant the node becomes active its waveform is at `phase` degrees. LFOs do not free-run across sound instances, so two instances of the same sound started at different times are phase-independent. This is a deliberate difference from a shared transport and keeps a sound instance's control behavior a function of its own age. A top-level `modulators.` entry may also declare `type: "lfo"` (PRD 33). The two are distinct constructs that merely share mathematics: a document modulator is a read-only shared source addressed as `modulators.*` (section 8.1) and uses `min`/`max` bounds, while this node is a graph-internal control source using `amplitude`/`polarity` and is not externally addressable (14.4). **Minimal example:** `{ "type": "lfo" }`. **Invalid case:** `{ "type": "lfo", "waveform": "custom" }` → `ERR_TYPE_MISMATCH`. ### 14.12 Sample-Hold (`sample-hold`) ```json { "type": "sample-hold", "rate": 2, "min": -1, "max": 1, "slew": "0ms" } ``` Control-only source (PRD 42). A `sample-hold` reaching `output` through any chain is `ERR_INVALID_ROUTE` (15.14). See 14.11 for the distinction from a top-level `modulators.` sample-and-hold. | Field | Type | Range | Default | ValueSpec | | --- | --- | --- | --- | --- | | `rate` | ValueSpec\ | `0.01` to `100` Hz | `2` | Yes (14.4 scope). | | `min` | ValueSpec\ | `-1000` to `1000` | `-1` | Yes (14.4 scope). | | `max` | ValueSpec\ | `-1000` to `1000` | `1` | Yes (14.4 scope). | | `slew` | DurationSpec 0.1 | `0ms` to `1s` | `0ms` | Sampled once at instantiation (14.4). | `min` resolving to a value greater than or equal to `max` is `ERR_INVALID_RANGE_ORDER`. **Tick schedule.** With resolved rate `r`, tick `k` (`k = 0, 1, 2, ...`) occurs at instance-relative time `k / r` seconds, measured from the node instance's start. Tick `0` draws the node's first held value; there is no pre-roll or default-held value before it. Because `rate` is resolved once (14.4), the tick period is fixed for the node instance's lifetime. **Draw and slew.** At each tick the node draws one uniform sample in `[min, max]`, then approaches it linearly from the previously held value over `slew` and holds until the next tick. A resolved `slew` longer than the tick period `1 / r` is clamped to that period, so the node always reaches its target before the next draw. **Procedural stream.** Per section 9.3, a subsystem may add a documented child key; this is that documentation. A `sample-hold` node instance uses domain `sound` with the stable instance key: ```text |node| ``` `` is the owning sound instance's key formed per section 9.3 from the sound definition ID and its invocation ordinal within the `sound` domain. `` is the node's key at recipe root, or, inside a component instance, the dot-joined chain of enclosing component-instance node keys followed by the node key (for example `chorus.voice-a.step`). The stream is created once when the node instance is created and is never reseeded; ticks consume exactly one sample each, in tick order. Two runs with the same normalized seed, the same exhibit, and the same logical input sequence therefore produce the same held sequence. **Minimal example:** `{ "type": "sample-hold" }`. **Invalid case:** `{ "type": "sample-hold", "min": 1, "max": -1 }` → `ERR_INVALID_RANGE_ORDER`. ### 14.13 Required traces before Phase 3a implementation is accepted 1. Each of the six node types validates its documented minimal example inside a complete graph object with zero diagnostics, and its documented invalid case with exactly the documented code. 2. A node object carrying an `id` field is `ERR_UNKNOWN_FIELD`, and a node keyed `output` is `ERR_INVALID_ID`. 3. Semantic validation rejects a frequency above `24000` without opening an `AudioContext`, and instantiation clamps against `min(24000, sampleRate x 0.45)` computed from the live sample rate, raising `WARN_AUDIO_RATE_CLAMP` rather than failing. 4. A node-field ValueSpec — for example `frequency: { "random": { "min": 220, "max": 440 } }` — samples once at instantiation and is stable for the node instance's lifetime. 5. An external `BindingSpec` targeting a node field fails with `ERR_UNSUPPORTED_TARGET`, confirming the 14.4 scope boundary. 6. Two `sample-hold` instances built from the same seed, exhibit, and invocation ordinal produce identical held sequences, and changing only the node key changes the sequence. ## 15. Audio Subsystem Contract — Processing, Routing, Components, and Buses (Phase 3b) This section completes the authoring surface of the audio subsystem: the processing and routing node types (PRD 43-51), the component instance node, audio routing and modulation (PRD 52-53), graph legality and authoring limits (PRD 55, and the authoring-time subset of PRD 58), reusable components (PRD 56), sound definitions and recipes (PRD 59), and buses (PRD 60). It builds directly on section 14: every node object declared here lives in a `nodes` map inside an audio graph object (14.3), its numeric fields follow the resolve-once scope of 14.4, its frequency fields follow the two-tier ceiling of 14.5, and it is externally unaddressable except through the modulation routes defined in 15.13. Audio automation precedence (PRD 54), the runtime lifecycle state machine (PRD 57), and the runtime half of the safety-limit and master-protection contract (PRD 58 voice ceilings, peak ceiling, numerical tolerance, release behavior, finite-sample handling) remain **Not yet specified** and arrive in Phase 3c. ### 15.1 Processing and routing node set | `type` | Class | Audio inputs | Audio output | Section | | --- | --- | --- | --- | --- | | `gain` | Processing | Many (summed) | Yes | 15.2 | | `filter` | Processing | Many (summed) | Yes | 15.3 | | `compressor` | Processing | Many (summed) | Yes | 15.4 | | `waveshaper` | Processing | Many (summed) | Yes | 15.5 | | `delay` | Processing | Many (summed) | Yes | 15.6 | | `reverb` | Processing | Many (summed) | Yes | 15.7 | | `stereo-pan` | Processing | Many (summed) | Yes | 15.8 | | `mixer` | Routing | Many (summed) | Yes | 15.9 | | `resonator` | Processing | Many (summed) | Yes | 15.10 | | `component` | Composite | Zero or one, per the component's `input` declaration | Yes | 15.11 | Every node that accepts audio input accepts any number of incoming audio routes and sums them; there is no per-input index in 0.1. Gain staging is explicit and uses `gain` nodes (PRD 50). ### 15.2 Gain (`gain`) ```json { "type": "gain", "gain": 1 } ``` | Field | Type | Range | Default | ValueSpec | Modulatable | | --- | --- | --- | --- | --- | :---: | | `gain` | ValueSpec\ | `0` to `4` | `1` | Yes (14.4 scope) | Yes, linear gain | Values above `1` are permitted for synthesis workflows and remain subject to master protection (PRD 43). **Minimal example:** `{ "type": "gain" }`. **Invalid case:** `{ "type": "gain", "gain": 8 }` → `ERR_OUT_OF_BOUNDS`. ### 15.3 Filter (`filter`) ```json { "type": "filter", "mode": "lowpass", "frequency": 800, "q": 0.7 } ``` | Field | Type | Range | Default | ValueSpec | Modulatable | | --- | --- | --- | --- | --- | :---: | | `mode` | enum | `lowpass`, `highpass`, `bandpass`, `notch`, `peaking`, `lowshelf`, `highshelf`, `allpass` | `lowpass` | No | No | | `frequency` | ValueSpec\ | `10` Hz to `audioMaxFrequency` (14.5) | `1000` | Yes | Yes, Hz | | `q` | ValueSpec\ | `0.0001` to `100` | `1` | Yes | Yes, unitless | | `gain` | ValueSpec\ | `-40` to `+40` dB | `0` | Yes | Yes, dB | | `detune` | ValueSpec\ | `-4800` to `+4800` cents | `0` | Yes | Yes, cents | `gain` is meaningful only for `peaking`, `lowshelf`, and `highshelf`; for the other modes it is ignored rather than rejected, so that a mode can be changed without restructuring the node. **Minimal example:** `{ "type": "filter" }`. **Invalid case:** `{ "type": "filter", "mode": "comb" }` → `ERR_TYPE_MISMATCH`. ### 15.4 Compressor (`compressor`) ```json { "type": "compressor", "threshold": -24, "knee": 30, "ratio": 12, "attack": "3ms", "release": "250ms" } ``` | Field | Type | Range | Default | ValueSpec | Modulatable | | --- | --- | --- | --- | --- | :---: | | `threshold` | ValueSpec\ | `-100` to `0` dB | `-24` | Yes | No | | `knee` | ValueSpec\ | `0` to `40` dB | `30` | Yes | No | | `ratio` | ValueSpec\ | `1` to `20` | `12` | Yes | No | | `attack` | DurationSpec 0.1 | `0ms` to `1s` | `3ms` | Sampled once | No | | `release` | DurationSpec 0.1 | `10ms` to `1s` | `250ms` | Sampled once | No | Audio-rate modulation of compressor parameters is not required in 0.1 (PRD 45); a modulation route to any compressor property is `ERR_UNSUPPORTED_TARGET`. **Minimal example:** `{ "type": "compressor" }`. **Invalid case:** `{ "type": "compressor", "ratio": 40 }` → `ERR_OUT_OF_BOUNDS`. ### 15.5 Waveshaper (`waveshaper`) ```json { "type": "waveshaper", "shape": "soft-clip", "amount": 0.5, "oversample": "2x" } ``` | Field | Type | Range | Default | ValueSpec | Modulatable | | --- | --- | --- | --- | --- | :---: | | `shape` | enum | `soft-clip`, `hard-clip`, `saturation` | `soft-clip` | No | No | | `amount` | ValueSpec\ | `0` to `1` | `0.5` | Yes | No | | `oversample` | enum | `none`, `2x`, `4x` | `none` | No | No | `amount` `0` is unity transfer for every shape, so a waveshaper can be authored inert and driven entirely by its resolved value. The exact transfer curve for each shape is runtime-defined but must be monotonic, odd-symmetric, and bounded to `[-1, 1]` for inputs in `[-1, 1]`. **Minimal example:** `{ "type": "waveshaper" }`. **Invalid case:** `{ "type": "waveshaper", "oversample": "8x" }` → `ERR_TYPE_MISMATCH`. ### 15.6 Delay (`delay`) ```json { "type": "delay", "time": "250ms", "feedback": 0.2, "mix": 0.5 } ``` | Field | Type | Range | Default | ValueSpec | Modulatable | | --- | --- | --- | --- | --- | :---: | | `time` | DurationSpec 0.1 | `0ms` to `10s` | `250ms` | Sampled once | Yes, milliseconds | | `feedback` | ValueSpec\ | `0` to `0.95` | `0.2` | Yes | No | | `mix` | ValueSpec\ | `0` to `1` | `0.5` | Yes | No | The feedback path is internal to the node and is controlled by the runtime (PRD 47). Authors may not build feedback by routing a node's output back into its own input chain; such a cycle is `ERR_CYCLIC_DEPENDENCY` under 15.14. `mix` is a dry/wet blend where `0` is fully dry and `1` is fully wet. A modulation route to `time` is expressed in milliseconds and is clamped so the resolved delay never leaves `[0ms, 10s]`. **Minimal example:** `{ "type": "delay" }`. **Invalid case:** `{ "type": "delay", "feedback": 1 }` → `ERR_OUT_OF_BOUNDS`. ### 15.7 Reverb (`reverb`) ```json { "type": "reverb", "size": 0.5, "decay": "2s", "damping": 0.5, "predelay": "0ms", "mix": 0.25 } ``` | Field | Type | Range | Default | ValueSpec | Modulatable | | --- | --- | --- | --- | --- | :---: | | `size` | ValueSpec\ | `0` to `1` | `0.5` | Yes | No | | `decay` | DurationSpec 0.1 | `50ms` to `30s` | `2s` | Sampled once | No | | `damping` | ValueSpec\ | `0` to `1` | `0.5` | Yes | No | | `predelay` | DurationSpec 0.1 | `0ms` to `500ms` | `0ms` | Sampled once | No | | `mix` | ValueSpec\ | `0` to `1` | `0.25` | Yes | No | The implementation is runtime-defined (PRD 48). The exhibit describes desired acoustic behavior, never Web Audio implementation details, and must not depend on a particular impulse response. Reverb properties are not modulatable in 0.1 because changing them requires rebuilding the underlying response. **Minimal example:** `{ "type": "reverb" }`. **Invalid case:** `{ "type": "reverb", "decay": "60s" }` → `ERR_OUT_OF_BOUNDS`. ### 15.8 Stereo pan (`stereo-pan`) ```json { "type": "stereo-pan", "pan": 0 } ``` | Field | Type | Range | Default | ValueSpec | Modulatable | | --- | --- | --- | --- | --- | :---: | | `pan` | ValueSpec\ | `-1` to `+1` | `0` | Yes | Yes, pan units | `-1` is full left, `0` is center, `+1` is full right (PRD 49). **Minimal example:** `{ "type": "stereo-pan" }`. **Invalid case:** `{ "type": "stereo-pan", "pan": 2 }` → `ERR_OUT_OF_BOUNDS`. ### 15.9 Mixer (`mixer`) ```json { "type": "mixer" } ``` A mixer accepts any number of audio inputs and exposes one audio output. It has no fields and no gain controls in 0.1; gain staging uses explicit `gain` nodes (PRD 50). Any property on a mixer node other than `type` is `ERR_UNKNOWN_FIELD`. A mixer is a convenience for graph readability: because every audio-accepting node already sums its inputs (15.1), a mixer is never required for correctness. ### 15.10 Resonator (`resonator`) ```json { "type": "resonator", "fundamental": 120, "modes": [ { "ratio": 1, "gain": 1, "decay": "1.2s" }, { "ratio": 2.7, "gain": 0.4, "decay": "800ms" } ], "mix": 1 } ``` | Field | Type | Range | Default | ValueSpec | Modulatable | | --- | --- | --- | --- | --- | :---: | | `fundamental` | ValueSpec\ | `0.1` Hz to `audioMaxFrequency` (14.5) | `120` | Yes | Yes, Hz | | `modes` | array | `1` to `16` entries | required | Per entry | No | | `mix` | ValueSpec\ | `0` to `1` | `1` | Yes | No | Each `modes` entry declares exactly one of `ratio` or `frequency`, never both and never neither (PRD 51): | Entry field | Type | Range | Default | Notes | | --- | --- | --- | --- | --- | | `ratio` | ValueSpec\ | `0.001` to `256` | — | Mode frequency as a multiple of the resolved `fundamental`. | | `frequency` | ValueSpec\ | `0.1` Hz to `audioMaxFrequency` | — | Absolute mode frequency; ignores `fundamental`. | | `gain` | ValueSpec\ | `0` to `1` | `1` | Linear amplitude of the mode. | | `decay` | DurationSpec 0.1 | `10ms` to `20s` | `1s` | Mode ring-down time to `-60` dB. | An entry declaring both `ratio` and `frequency`, or neither, is `ERR_SCHEMA_VALIDATION`. More than 16 entries is `ERR_NODE_LIMIT_EXCEEDED`. A mode whose resolved frequency exceeds `audioMaxFrequency` at instantiation is omitted, matching the custom-partial rule of 14.7. A resonator provides generic acoustic resonance and is never named or shaped after a themed sound (PRD 51). **Minimal example:** `{ "type": "resonator", "modes": [{ "ratio": 1 }] }`. **Invalid case:** `{ "type": "resonator", "modes": [{ "ratio": 1, "frequency": 200 }] }` → `ERR_SCHEMA_VALIDATION`. ### 15.11 Component instance (`component`) ```json { "type": "component", "use": "voice", "values": { "pitch": 330 } } ``` | Field | Type | Required | Notes | | --- | --- | :---: | --- | | `use` | string | Yes | ID of a declared `components.audio.`. An unknown ID is `ERR_INVALID_REFERENCE`. | | `values` | object | No | Values for the component's exposed parameters, one ValueSpec each, resolved once at instantiation (14.4). | A key in `values` that the component does not expose is `ERR_UNKNOWN_FIELD`. An exposed parameter that has no `default` and receives no value is `ERR_SCHEMA_VALIDATION`. A supplied value outside the exposed parameter's declared range is `ERR_OUT_OF_BOUNDS`. A component instance behaves as an ordinary node in its enclosing graph: it exposes one audio output, and it accepts audio input if and only if the component declares `input: true` (15.15). Routing audio into a component that declares no input is `ERR_INVALID_ROUTE`. Its exposed parameters are modulation targets addressed as `.` (15.13). Nothing else inside the component is reachable from outside (PRD 56). ### 15.12 Audio routing A `routes` entry (14.3) is one of two forms, distinguished by whether `to` names a node or a node property (PRD 52): ```json { "from": "tone", "to": "filter" } ``` ```json { "from": "vibrato", "to": "tone.frequency", "depth": 18 } ``` | Field | Type | Required | Notes | | --- | --- | :---: | --- | | `from` | string | Yes | A node key in the same graph, or the reserved `input` inside a component graph that declares `input: true`. `output` may never be a `from` (`ERR_INVALID_ROUTE`). | | `to` | string | Yes | Audio route: a node key in the same graph, or the reserved `output`. Modulation route: `.`. | | `depth` | ValueSpec\ | Modulation only | Required on a modulation route; present on an audio route it is `ERR_UNKNOWN_FIELD`. Resolved once at instantiation (14.4). | `from` or `to` naming a node key that the graph does not declare is `ERR_INVALID_REFERENCE`. A route whose `from` and `to` resolve to the same node is `ERR_INVALID_ROUTE`. Duplicate identical audio routes are collapsed to one and raise no diagnostic; duplicate modulation routes to the same property are summed (15.13), not collapsed. Because every audio-accepting node sums its inputs (15.1), route order within `routes` never affects the audible result. Route order does fix the depth-first ValueSpec sampling order of `depth` values within a graph, per section 9.3. ### 15.13 Modulation semantics Modulation depth is expressed in the target property's own unit (PRD 53). The modulatable property registry for 0.1 is exactly: | Node type | Property | Depth unit | | --- | --- | --- | | `oscillator` | `frequency` | Hz | | `oscillator` | `detune` | cents | | `gain` | `gain` | linear gain | | `filter` | `frequency` | Hz | | `filter` | `q` | unitless | | `filter` | `gain` | dB | | `filter` | `detune` | cents | | `delay` | `time` | milliseconds | | `stereo-pan` | `pan` | pan units | | `resonator` | `fundamental` | Hz | | `component` | `` | the exposed parameter's declared unit | A modulation route to any property absent from this table — including every `compressor`, `reverb`, `waveshaper`, `mixer`, `impulse`, and `noise` property, and every property of a control source — is `ERR_UNSUPPORTED_TARGET`. Merely being numeric does not grant modulation support, matching section 8.1's rule for the shared registry. **Sources.** A modulation route's `from` must be a control source (`constant`, `lfo`, `sample-hold`) or an `oscillator` used at audio rate (PRD 41). Any other node as a modulation source is `ERR_INVALID_ROUTE`. **Summation and clamping.** For a modulated property with resolved base value `b` and modulation routes `1..n` whose source outputs at the current instant are `s_i` with resolved depths `d_i`: ```text value = clamp(b + sum(s_i x d_i), property minimum, property maximum) ``` Multiple legal modulation routes are summed (PRD 53). The clamp uses the property's declared range from its node contract and is the audio application of the safety-clamp stage of section 8.1; it is not a separate precedence system. A control source's output is its own value in its own units before scaling: an `lfo` with `amplitude: 1` and `polarity: "bipolar"` contributes `[-d, +d]` to its target. Automation (PRD 54) sits between binding and override in the shared pipeline and is **Not yet specified**; in Phase 3b the resolution of a node property is exactly base, then modulation sum, then safety clamp. ### 15.14 Graph legality and authoring limits The final expanded audio graph of a sound — the recipe graph with every component instance inlined — must satisfy all of the following (PRD 55). Each check names the diagnostic it emits. | # | Rule | Diagnostic | | --- | --- | --- | | 1 | Every node key matches the identifier rule and is not `output` | `ERR_INVALID_ID` | | 2 | Every node `type` is in Audio Graph Node Set 0.1 | `ERR_INVALID_NODE_TYPE` | | 3 | Every route `from` and `to` resolves to a declared node, `output`, or a legal `input` | `ERR_INVALID_REFERENCE` | | 4 | `output` is sink-only and never a route `from` | `ERR_INVALID_ROUTE` | | 5 | A control source (`constant`, `lfo`, `sample-hold`) never reaches `output` through any chain of audio routes | `ERR_INVALID_ROUTE` | | 6 | A source node (`oscillator`, `noise`, `impulse`, and every control source) never receives an audio route | `ERR_INVALID_ROUTE` | | 7 | The audio route graph is acyclic | `ERR_CYCLIC_DEPENDENCY` | | 8 | The modulation dependency graph is acyclic | `ERR_CYCLIC_DEPENDENCY` | | 9 | At least one audio route chain from a non-control source reaches `output` | `ERR_NO_AUDIBLE_PATH` | | 10 | A component never instantiates itself, directly or transitively | `ERR_COMPONENT_RECURSION` | | 11 | Component nesting depth is at most `8` | `ERR_COMPONENT_RECURSION` | | 12 | Graph limits are respected | `ERR_NODE_LIMIT_EXCEEDED` | Rule 5 is the enforcement point for the control-only status asserted in 14.10, 14.11, and 14.12. Rule 6 makes source nodes true graph roots. Rule 7 is what forbids unrestricted author-built feedback (PRD 47); a `delay` node's internal feedback path is not part of the route graph and does not violate it. The authoring-time limits enforced in Phase 3b are the subset of PRD 58 that a document can violate on its own: | Limit | Value | | --- | --- | | Expanded nodes per sound | `128` | | Routes per sound | `256` | | Component nesting depth | `8` | | Resonator modes | `16` | | Custom oscillator partials | `64` | The remaining PRD 58 limits — approximate one-shot voices (`64`), approximate continuous sounds (`16`), automation tracks (`64`), and automation points (`256`) — are runtime ceilings rather than document properties and belong to Phase 3c with the lifecycle contract. Master protection likewise remains Phase 3c: this section does not establish that the protection contract passes, and the presence of a compressor in a graph never constitutes master protection (PRD 58). ### 15.15 Audio components ```json { "components": { "audio": { "voice": { "parameters": { "pitch": { "type": "number", "default": 220, "min": 20, "max": 2000 } }, "input": false, "nodes": { "tone": { "type": "oscillator", "frequency": { "ref": "inputs.pitch" } }, "level": { "type": "gain", "gain": 0.3 } }, "routes": [ { "from": "tone", "to": "level" }, { "from": "level", "to": "output" } ] } } } } ``` A `components.audio.` entry is an audio graph object (14.3) with two additional fields: | Field | Type | Required | Notes | | --- | --- | :---: | --- | | `parameters` | object keyed by parameter ID | No | Exposed numeric parameters. Each is `{ "type": "number", "default"?: number, "min"?: number, "max"?: number, "unit"?: string }`. Only `number` is permitted in 0.1. | | `input` | boolean | No (default `false`) | When `true`, the reserved node key `input` is available as a route `from` inside the graph and the component instance accepts one audio input. | **Component-local references.** Inside a component graph, and only there, a ValueSpec may use the reference form `{ "ref": "inputs." }` to read the instance's value for an exposed parameter. `inputs.*` is a component-graph-scoped namespace: used anywhere else it is `ERR_INVALID_REFERENCE`, and it is not added to the section 1.3 document namespaces or the section 8.1 target table. A reference to an undeclared parameter is `ERR_INVALID_REFERENCE`. **Encapsulation.** External graphs may access only the component's audio input, its audio output, and its explicitly exposed parameters (PRD 56). A reference or route reaching an internal node key from outside is `ERR_INVALID_REFERENCE`. Node keys inside a component are scoped to that component and may repeat keys used in the enclosing graph without conflict; the expansion path of 14.12 disambiguates them for PRNG stream derivation. Component nesting is limited to `8` levels and recursion is prohibited (15.14, rules 10 and 11). ### 15.16 Sound definitions and recipes ```json { "sounds": { "relay-click": { "name": "Relay Click", "tags": ["mechanical", "electrical"], "usage": ["automatic", "manual", "scenario"], "cadence": { "class": "routine" }, "bus": "effects", "recipe": { "mode": "oneshot", "nodes": { "hit": { "type": "impulse" }, "body": { "type": "resonator", "modes": [{ "ratio": 1 }] } }, "routes": [{ "from": "hit", "to": "body" }, { "from": "body", "to": "output" }] } } } } ``` A `sounds.` entry separates semantic metadata from synthesis (PRD 59): | Field | Type | Required | Notes | | --- | --- | :---: | --- | | `name` | string | Yes | Display name, at most `128` characters. | | `tags` | array of string | No | At most `16` entries of at most `32` characters. | | `usage` | array of enum | No | Any of `automatic`, `manual`, `scenario`; defaults to `["automatic"]`. Usage and cadence are separate concerns (PRD 62). | | `cadence` | object | No | Cadence assignment. Its fields are specified by the Cadence contract in Phase 5; in 0.1 Phase 3b it is validated only as an object. | | `bus` | string | No | A declared `audio.buses.`. An undeclared bus is `ERR_INVALID_REFERENCE`. Omitted, the sound routes to the engine master. | | `recipe` | object | Yes | An audio graph object plus the fields below, or `{ "use": "" }` naming an `audio.recipes.`. | A recipe graph object adds one field to the graph shape of 14.3: | Field | Type | Required | Notes | | --- | --- | :---: | --- | | `mode` | enum | No (default `oneshot`) | `oneshot` or `continuous` (PRD 57). A `oneshot` recipe must have a determinable ending; the runtime state machine that enforces it is Phase 3c. | `audio.recipes.` holds the same shape and exists so several sounds can share one graph. A `recipe` object containing both `use` and graph fields is `ERR_SCHEMA_VALIDATION`, and a `use` naming an undeclared recipe is `ERR_INVALID_REFERENCE`. ### 15.17 Audio buses and master ```json { "audio": { "buses": { "ambient": { "gain": 1 }, "effects": { "gain": 1 } } } } ``` | Field | Type | Required | Notes | | --- | --- | :---: | --- | | `gain` | ValueSpec\ | No (default `1`) | Range `[0, 4]`, matching the section 8.1 safety clamp for `audio.buses..gain`. | A bus is a named summing point with one gain stage. `audio.buses..gain` is the one audio target in the section 8.1 shared registry, and it remains the supported surface for binding, automation, override, and additive modulation of audio level from outside a graph. Bus processing beyond gain is not part of 0.1. `master` is engine-provided and may not be declared as a bus ID (`ERR_INVALID_ID`). The `audio.master` object is reserved for the Phase 3c master-protection contract and must be absent from a 0.1 document; present, it is `ERR_UNKNOWN_FIELD`. ### 15.18 New diagnostic codes The following codes are added to the section 7 table, which remains the single authoritative list: | Error Code | Stage | Cause | | :--- | :--- | :--- | | `ERR_INVALID_ROUTE` | Semantic | An audio or modulation route is structurally resolvable but illegal under 15.14. | | `ERR_NO_AUDIBLE_PATH` | Semantic | No chain of audio routes from a non-control source reaches `output`. | | `ERR_COMPONENT_RECURSION` | Semantic | A component instantiates itself transitively, or component nesting exceeds `8` levels. | | `WARN_AUDIO_RATE_CLAMP` | Runtime | A frequency field was clamped to the device's `audioMaxFrequency` at node instantiation. | ### 15.19 Required traces before Phase 3b implementation is accepted 1. Each processing, routing, and composite node type validates its documented minimal example inside a complete graph with zero diagnostics, and its documented invalid case with exactly the documented code. 2. Every rule in the 15.14 table is exercised by at least one fixture that emits exactly the named diagnostic, and by one passing fixture that does not. 3. A graph whose only path to `output` originates at an `lfo`, `constant`, or `sample-hold` fails rule 5, and the same graph with an `oscillator` source passes. 4. A component instance expands correctly: `inputs.*` resolves to the instance's supplied values, internal node keys are unreachable from outside, an exposed parameter is a legal modulation target, and a self-referencing component is `ERR_COMPONENT_RECURSION`. 5. Two modulation routes onto one property sum and then clamp to the property's declared range. 6. A graph exceeding `128` expanded nodes or `256` routes after component expansion is `ERR_NODE_LIMIT_EXCEEDED`, while a graph at exactly those counts passes. 7. Expansion and validation run with no `AudioContext`, confirming the 14.5 separation between semantic validation and instantiation.