Files
XZBT/docs/XZBT_0-1_Format_Specification.md
T
LabyricornandClaude Opus 5 50fb72c0e8 docs(audio): close three defects in the phase 3c contract
Re-read section 16 before handing the implementation slice onward. Three
defects, all of the same classes the Phase 3a review turned up.

The 16.5 ending-bound table gave a contribution for every node type
except the one that contains other nodes, so a one-shot whose tail lived
inside a component had no defined bound. Components now contribute the
bound of their own graph by the same rule, terminating on the existing
nesting cap.

The 16.3 state machine offered no exit from CREATED for a stop arriving
before scheduling except FAILED, which would have reported an ordinary
cancellation as a fault. Permit CREATED -> FINISHED, and say why it
differs from SCHEDULED -> RELEASING: a created instance is connected to
nothing, so there is no signal to ramp down.

Automation point ordering was a stage conflation. Points had to be in
strictly increasing `at` order while `at` was a DurationSpec, which
section 6.2 permits to be a procedural TimeSpec resolved at
instantiation — so the ordering rule could not have been enforced at the
semantic stage where it was filed. This is structurally the same defect
as the audioMaxFrequency one closed in Phase 3a. Fix `at` as a duration
literal; point values remain full ValueSpecs.

Still contract only. No runtime change, and no sound has been heard from
any build.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FWPdCqKaaDnP9NC3JAwh6
2026-09-05 23:29:23 +00:00

108 KiB

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 contract is complete (Phase 3a sources/control sources, Phase 3b processing/routing/components/buses, Phase 3c automation/lifecycle/protection); master-protection values remain provisional pending GC6 measurement, and the remaining subsystem contracts are in progress Related resources: PRD, decisions, verification

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:
    ^[a-z][a-z0-9_-]*$
    
  • Reference Path Syntax: Dot-delimited path referencing a target property or namespace:
    <namespace>.<resource_id>[.<property>]
    
    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.<id>)

Parameters represent user-configurable settings. They have documented defaults and bounds:

{
  "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.<id>)

State variables represent simulation-owned state. They are initialized at launch and manipulated by runtime events and scenarios:

{
  "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:

440.0

4.2 Reference (ref)

Reads the currently resolved value of a declared target:

{ "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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.
    { "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.
    { "or": [ { "op": "lt", "left": { "ref": "state.energy" }, "right": 0.1 }, { "op": "eq", "left": { "ref": "state.alarm" }, "right": true } ] }
    
  • Negation (not): Inverts the child condition.
    { "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:

{
  "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.
ERR_AUTOMATION_CONFLICT Semantic More than one automation track directly controls one property in a recipe instance.
ERR_INDETERMINATE_ONESHOT Semantic A oneshot recipe has no computable finite ending; its audible path begins at an unbounded source.
WARN_AUTOMATION_FALLBACK Runtime An exponential automation segment had a zero or sign-crossing endpoint and fell back to linear interpolation.
WARN_VOICE_LIMIT Runtime A voice ceiling was reached; an instance was evicted or a request refused.
WARN_AUDIO_NONFINITE Runtime A non-finite sample reached the master chain and the containing block was muted.
WARN_AUDIO_UNAVAILABLE Runtime No audio device is available; the performance continues silently.

8. Shared value resolution, bindings, and transitions

8.1 Resolution pipeline and target capabilities

For each supported target, evaluate:

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.<id> 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.<id> Declared primitive; current state value Yes No Yes No Declared min/max
audio.buses.<id>.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.

{
  "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:

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-<activationSequence>. 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]:

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:

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:

xzbt-0.1\0<root-seed-decimal>\0<domain>\0<stable-instance-key>

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 Complete (Rev 0.4 / Phase 3a-3c): units, graph objects, all sixteen node types, routing, modulation, graph legality, authoring limits, components, sounds/recipes, buses, automation tracks and precedence, lifecycle states and release, determinable one-shot endings, voice ceilings, unlock and pause behavior, and the master-protection contract shape. Master-protection values (peak ceiling, tolerance, release behavior) are provisional pending GC6 measurement
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). Automation tracks and precedence (PRD 54), the runtime lifecycle state machine and release (PRD 57), determinable one-shot endings, voice ceilings, master protection (PRD 58), and unlock and pause behavior (PRD 117-118) are specified in section 16 (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:

{
  "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).
automation array No (defaults to []) Automation tracks, specified in 16.1. Declared on the graph object, never on a node.

Audio graph objects are embedded at exactly three places, whose surrounding fields are specified in Phase 3b:

  • audio.recipes.<recipe-id> — a named, reusable recipe graph (15.16).
  • sounds.<sound-id>.recipe — an inline recipe graph (15.16, PRD 59).
  • components.audio.<component-id> — 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.<id>.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 specified in sections 16.1 and 16.2. A track is declared on the graph object alongside nodes and routes, never on a node, so an automation field on a node object remains ERR_UNKNOWN_FIELD. Automation does not widen the external surface of 14.4: a track is authored inside the graph that owns the property, not in the document's binding list.

14.5 Audio frequency ceiling and validation staging

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)

{
  "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<number> 0.1 Hz to audioMaxFrequency (14.5) 440 Yes (14.4 scope).
detune ValueSpec<number> -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)

{ "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)

{
  "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<number> 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)

{ "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<number> -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)

{
  "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<number> 0.001 to 40 Hz 1 Yes (14.4 scope).
amplitude ValueSpec<number> 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<number> 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.<id> 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)

{
  "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.<id> sample-and-hold.

Field Type Range Default ValueSpec
rate ValueSpec<number> 0.01 to 100 Hz 2 Yes (14.4 scope).
min ValueSpec<number> -1000 to 1000 -1 Yes (14.4 scope).
max ValueSpec<number> -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:

<sound-instance-key>|node|<node-path>

<sound-instance-key> 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. <node-path> 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.

Automation tracks and precedence (PRD 54), the runtime lifecycle state machine and release (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) are specified in section 16 (Phase 3c). The master-protection values there remain provisional pending GC6 measurement.

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)

{ "type": "gain", "gain": 1 }
Field Type Range Default ValueSpec Modulatable
gain ValueSpec<number> 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)

{ "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<number> 10 Hz to audioMaxFrequency (14.5) 1000 Yes Yes, Hz
q ValueSpec<number> 0.0001 to 100 1 Yes Yes, unitless
gain ValueSpec<number> -40 to +40 dB 0 Yes Yes, dB
detune ValueSpec<number> -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)

{ "type": "compressor", "threshold": -24, "knee": 30, "ratio": 12, "attack": "3ms", "release": "250ms" }
Field Type Range Default ValueSpec Modulatable
threshold ValueSpec<number> -100 to 0 dB -24 Yes No
knee ValueSpec<number> 0 to 40 dB 30 Yes No
ratio ValueSpec<number> 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)

{ "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<number> 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)

{ "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<number> 0 to 0.95 0.2 Yes No
mix ValueSpec<number> 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)

{ "type": "reverb", "size": 0.5, "decay": "2s", "damping": 0.5, "predelay": "0ms", "mix": 0.25 }
Field Type Range Default ValueSpec Modulatable
size ValueSpec<number> 0 to 1 0.5 Yes No
decay DurationSpec 0.1 50ms to 30s 2s Sampled once No
damping ValueSpec<number> 0 to 1 0.5 Yes No
predelay DurationSpec 0.1 0ms to 500ms 0ms Sampled once No
mix ValueSpec<number> 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)

{ "type": "stereo-pan", "pan": 0 }
Field Type Range Default ValueSpec Modulatable
pan ValueSpec<number> -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)

{ "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)

{
  "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<number> 0.1 Hz to audioMaxFrequency (14.5) 120 Yes Yes, Hz
modes array 1 to 16 entries required Per entry No
mix ValueSpec<number> 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<number> 0.001 to 256 Mode frequency as a multiple of the resolved fundamental.
frequency ValueSpec<number> 0.1 Hz to audioMaxFrequency Absolute mode frequency; ignores fundamental.
gain ValueSpec<number> 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)

{ "type": "component", "use": "voice", "values": { "pitch": 330 } }
Field Type Required Notes
use string Yes ID of a declared components.audio.<id>. 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 <component-node-key>.<parameter-id> (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):

{ "from": "tone", "to": "filter" }
{ "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: <node-key>.<property>.
depth ValueSpec<number> 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 <exposed-parameter-id> 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:

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 specified in sections 16.1 and 16.2. With no automation track on a property, its resolution 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

{
  "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.<id> 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.<parameter-id>" } 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

{
  "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.<id> 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.<id>. 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": "<recipe-id>" } naming an audio.recipes.<id>.

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.<recipe-id> 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

{
  "audio": {
    "buses": {
      "ambient": { "gain": 1 },
      "effects": { "gain": 1 }
    }
  }
}
Field Type Required Notes
gain ValueSpec<number> No (default 1) Range [0, 4], matching the section 8.1 safety clamp for audio.buses.<id>.gain.

A bus is a named summing point with one gain stage. audio.buses.<id>.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.

16. Audio Subsystem Contract — Automation, Lifecycle, and Protection (Phase 3c)

This section closes the audio subsystem. It covers automation tracks and their place in the shared resolution pipeline (PRD 54), the runtime lifecycle state machine and release behavior (PRD 57), determinable one-shot endings, voice ceilings and eviction, master output protection (PRD 58, 120), and unlock and pause behavior for audio (PRD 117-118).

Sections 14 and 15 define what an exhibit may declare. This section defines what the runtime does with it over time. Where an earlier section deferred a rule to "Phase 3c", this section is the referent.

Implementation status. This section is the Phase 3c contract; the runtime does not yet implement it. Until slices 3c-2 and 3c-3 land, a document declaring automation or release is rejected with ERR_UNKNOWN_FIELD by the current validator, sound instances have no lifecycle state machine, and no voice ceiling is enforced. The implementation plan records the slice order.

16.1 Automation tracks

An automation track drives one node property along an authored curve measured from the owning sound instance's start. Tracks are declared in the automation array of a graph object (14.3), alongside nodes and routes, and are therefore available in recipe graphs and component graphs alike:

{
  "automation": [
    {
      "target": "level.gain",
      "mode": "absolute",
      "interpolation": "smooth",
      "points": [
        { "at": "0ms", "value": 0 },
        { "at": "800ms", "value": 1 },
        { "at": "4s", "value": 0.6 }
      ]
    }
  ]
}
Field Type Required Notes
target string Yes <node-key>.<property>, resolved in the same graph. Follows the encapsulation rules of 15.15: a component's internals are unreachable, and a component instance exposes only its declared parameters.
mode enum No (default absolute) absolute, offset, or scale.
interpolation enum No (default linear) step, linear, exponential, or smooth.
points array Yes 2 to 256 breakpoints, each { "at": <duration literal>, "value": ValueSpec<number> }.

automation is an array rather than a keyed map because a track has no identity an author needs to reference. It defaults to [].

Targets. A track may address any property in the 15.13 modulatable registry, and no other. A track targeting a property absent from that registry is ERR_UNSUPPORTED_TARGET; the registry is deliberately the same one modulation uses, so a property is either time-varying or it is not, and the two mechanisms never disagree about which. A target naming an undeclared node is ERR_INVALID_REFERENCE.

Exclusivity. Only one automation track may directly control a property in a recipe instance (PRD 54). Two tracks addressing the same expanded target are ERR_AUTOMATION_CONFLICT. This is the automation counterpart of the ERR_CONFLICTING_BINDING rule in section 8.2, and for the same reason: two writers to one scalar has no defined answer. Modulation is unaffected — multiple modulation routes onto one property still sum (15.13), because summation is their defined answer.

Points. at is measured from the node instance's start and is a duration literal only (section 6.1). Unlike every other duration in this contract it may not be a procedural TimeSpec (section 6.2), and a TimeSpec there is ERR_TYPE_MISMATCH. The reason is the staging rule of 14.5: points must be in strictly increasing at order, and an order over values that resolve randomly at instantiation could not be checked at the semantic stage at all. Fixing at as a literal keeps the ordering rule decidable at import, where a malformed curve should be caught. A track's values remain full ValueSpecs and may be random; it is only the curve's shape in time that is authored, not sampled.

Equal or decreasing times are ERR_INVALID_RANGE_ORDER. Fewer than two points is ERR_SCHEMA_VALIDATION — a single point is a constant and belongs in the property's base value. Before the first point the track holds the first point's value; after the last point it holds the last point's value and does not loop.

Modes. Given the property's resolved base value b and the track's current curve value a, the track contributes:

mode Contribution Use
absolute a The curve is the value; the base is ignored.
offset b + a The curve displaces the authored base in the property's own unit.
scale b x a The curve multiplies the authored base.

Interpolation. Between two points with values v0 and v1 and normalized progress t in [0, 1):

interpolation Value Notes
step v0 Holds until the next point, then jumps.
linear v0 + (v1 - v0) x t
exponential v0 x (v1 / v0)^t Requires both endpoints strictly positive and same-signed; a segment with a zero or sign-crossing endpoint falls back to linear and raises WARN_AUTOMATION_FALLBACK once per track. A fallback is preferable to failing, because a legal base value can resolve to zero at instantiation.
smooth v0 + (v1 - v0) x (3t^2 - 2t^3) Smoothstep; zero first derivative at both endpoints.

Limits. At most 64 automation tracks and 256 total automation points per expanded sound (PRD 58). Exceeding either is ERR_NODE_LIMIT_EXCEEDED.

16.2 Audio automation precedence

Section 8.1 fixes the shared resolution pipeline. This section states its audio application; it is not a competing system (PRD 54). For a node property:

base ValueSpec -> binding (where supported) -> automation -> winning override -> modulation sum -> safety clamp -> engine parameter

Three consequences follow, and they are the whole content of this subsection.

Binding remains unsupported for node properties. Section 14.4 stands: an external BindingSpec targeting a node field is ERR_UNSUPPORTED_TARGET. Automation does not change this, because a track is declared inside the graph that owns the property, not in the document's binding list. Adding automation to a subsystem never widens that subsystem's external surface. audio.buses.<id>.gain remains the one audio target in the shared registry, and it takes binding, automation, override, and modulation as section 8.1 already states.

Underlying automation continues while overridden (PRD 54). An override masks the automation stage's output for as long as it wins; it does not pause, reset, or rewind the track. When the override releases, the property returns to whatever the track has reached by then, not to the value it held when the override took hold. This mirrors the parameter-masking rule of section 8.1, where releasing an override returns toward the current stored value rather than an obsolete snapshot.

Every stage a target does not expose is absent, not an identity hook. A property outside the 15.13 registry has no automation stage and no modulation stage. Being numeric grants nothing.

16.3 Lifecycle states

A sound instance occupies exactly one state (PRD 57):

State Meaning Audio produced
CREATED Graph expanded, values resolved, nodes constructed, nothing connected to a bus. No
SCHEDULED Start time fixed on the audio clock; awaiting it. No
ACTIVE Running and audible. Yes
RELEASING Release envelope running; no new work accepted. Yes, decaying
FINISHED Release complete; output silent; resources not yet reclaimed. No
DISPOSED Every node, connection, automation track, buffer, and subscription released. Terminal. No
FAILED Construction or activation raised; resources reclaimed as for DISPOSED. Terminal. No

Permitted transitions, and nothing else:

CREATED    -> SCHEDULED | FINISHED | FAILED
SCHEDULED  -> ACTIVE | RELEASING | FAILED
ACTIVE     -> RELEASING | FINISHED | FAILED
RELEASING  -> FINISHED | FAILED
FINISHED   -> DISPOSED

A stop request in SCHEDULED goes to RELEASING rather than straight to FINISHED, so a single code path handles teardown whether or not the voice ever sounded: a scheduled instance is already connected to its bus and may begin sounding at any moment on the audio clock. A stop request in CREATED goes straight to FINISHED instead, because a created instance is not connected to anything and no signal can escape it; ramping a gain that reaches no bus would be ceremony, not safety. ACTIVE -> FINISHED without a release is reserved for a one-shot that has reached its determinable ending, where the envelope has already returned to zero and a further release would be redundant. DISPOSED and FAILED are terminal; a second stop on a disposed instance is a no-op, never an error.

Every state change is observable to the runtime's own diagnostics, but state names are not exposed to exhibits. An exhibit describes desired behavior, not runtime bookkeeping.

16.4 Release and the internal release gain

The runtime provides an internal release gain for every sound instance, whether or not the exhibit authored one (PRD 57). It is the last node before the instance's bus, it is engine-owned, and no route can bypass or address it.

The default release is 50ms. A recipe graph object (15.16) may declare one further field, release, a DurationSpec in 0ms to 10s; 0ms is permitted and means an immediate cut, which is an author's explicit choice rather than a default. release is a recipe field, not a graph-object field: a component graph does not declare it, because release belongs to the sound instance and not to any part of it. Entering RELEASING ramps the internal release gain linearly from its current value to zero over the release duration, and FINISHED follows at the end of that ramp.

Release is unconditional. It runs on exhibit deactivation, on voice eviction, on pause-induced teardown, and on failure, so no path exists that stops a voice by disconnecting an already-running source. This is what keeps the click-and-pop surface to a single, testable code path.

Disposal must release nodes, connections, automation tracks, buffers, and subscriptions (PRD 57). A FINISHED instance that has not reached DISPOSED still holds resources and still counts against the voice ceilings of 16.6.

16.5 Determinable one-shot endings

A oneshot recipe must have a determinable ending (PRD 57). "Determinable" means the runtime can compute a finite upper bound on the instance's audible duration at instantiation, from the resolved graph alone, without observing output.

The bound is computed as the longest path from any source to output, where each node contributes:

Node Contribution to the bound
impulse its resolved duration
oscillator, noise unbounded — these run until stopped
constant, lfo, sample-hold zero; control sources are not on an audible path (15.14 rule 5)
delay time x ceil(log(1/1000) / log(feedback)) for feedback > 0, else time; the time for the internal feedback path to fall 60 dB
reverb predelay + decay
resonator the longest decay among its retained modes
component the bound of the component's own graph, computed by this same rule over its internal nodes, from its sources or its input to its output
every other node zero; they colour a signal without extending it

The instance's ending is the maximum over all source-to-output paths of the sum of contributions along that path, plus the release duration of 16.4. Component contributions recurse, and the recursion terminates because component nesting is capped at 8 levels (15.14 rule 11). A component whose internal graph is itself unbounded makes every path through it unbounded, exactly as an unbounded source does at recipe root.

A oneshot recipe whose bound is unbounded — that is, one whose audible path begins at an oscillator or noise source — is ERR_INDETERMINATE_ONESHOT at validation. The author's remedies are to declare mode: "continuous" and stop the sound explicitly, or to gate the source through an impulse-driven path. This is a semantic error rather than a runtime one because it is decidable from the document, and catching it at import is the difference between a rejected exhibit and a voice that never frees itself.

A continuous recipe has no ending requirement and runs until explicitly stopped.

16.6 Voice ceilings and eviction

Approximate ceilings (PRD 58):

Ceiling Value
One-shot voices 64
Continuous sounds 16

These are approximate and may be lowered by the runtime on a weaker device (PRD 58, 120). They are never raised above these values by a document; an exhibit cannot request a larger budget. A voice counts against its ceiling from CREATED until DISPOSED, so instances lingering in FINISHED still occupy budget — which is why disposal is part of the contract rather than an optimization.

When a new instance would exceed its ceiling, the runtime applies this policy in order and stops at the first candidate:

  1. Dispose the oldest instance already in FINISHED.
  2. Evict the oldest instance in RELEASING.
  3. For a one-shot request only: evict the oldest ACTIVE one-shot by starting its release.
  4. Otherwise refuse the new instance.

Eviction always releases (16.4); it never hard-stops. A one-shot request never evicts a continuous sound, and a continuous request never evicts a one-shot — the two budgets are independent, because a bed of ambience and a burst of transients fail differently and stealing across the boundary produces the worse failure in both directions.

Every eviction and every refusal raises WARN_VOICE_LIMIT once, naming the sound and the ceiling. A refusal is not an error: an exhibit that asks for a sixty-fifth simultaneous transient is behaving legally, and the runtime's job is to stay stable and say so.

Resource ceilings are centralized rather than scattered through subsystems (PRD 120). Every limit in sections 14-16 lives in one runtime table.

16.7 Master output protection

Every audible signal passes through engine-controlled master protection, and an exhibit cannot bypass it (PRD 58). The chain is engine-owned, is the only path from any bus to the output device, and is not addressable from a document: no route, binding, override, automation, or action can reach it. audio.master remains absent from a 0.1 document (15.17).

The protection contract has four parts. Their shape is normative now; their values are provisional and are confirmed by measurement under GC6 before audio acceptance (PRD 58).

Part Contract Status
Digital output peak ceiling The absolute sample value at the output device never exceeds the ceiling. Provisional ceiling -1.0 dBFS (0.891 linear). Provisional; pending GC6
Numerical tolerance Measured peak may exceed the ceiling by no more than the tolerance across a full measurement window. Provisional tolerance 0.1 dB. Provisional; pending GC6
Release behavior Protection gain reduction recovers smoothly and never produces an audible pump on a sustained bed or a click on a transient. Verified by listening, not by a number alone. Provisional; pending GC6
Finite-sample handling A non-finite sample (NaN or infinity) reaching the master chain is replaced with silence for that sample, the containing block is muted, and WARN_AUDIO_NONFINITE is raised once per instance. The count of affected blocks is recorded for the acceptance run. Normative

Only finite-sample handling is settled, because it is a correctness rule rather than a measured threshold: a NaN in the output buffer is never acceptable at any ceiling, and silencing it is strictly better than propagating it. The other three require real output on real hardware.

The presence of a compressor does not establish that this contract passes (PRD 58). A runtime may implement the chain with a limiter, a compressor, a soft clipper, or any combination; what it may not do is claim the contract on the strength of having built one. Acceptance requires the measured peak, the measured tolerance across worst-case overlapping recipes, and listening observations for clicks, clipping, and pumping.

16.8 Unlock behavior

Browsers may require user interaction before producing audio (PRD 118). The runtime treats the first user interaction that engages the exhibit as the point at which audio is initialized or resumed. Visual rendering may begin before audio authorization, and the UI must clearly represent muted or unavailable audio state.

When audio unlocks after visual startup, it aligns to the current logical position rather than replaying the interval it missed:

  • Expired one-shot invocations are not replayed. Every one-shot whose invocation time has already passed is discarded, and INFO_AUDIO_UNLOCK_SKIP is raised once for the whole batch, carrying the count. One diagnostic for the batch, not one per skipped sound: a long pre-unlock interval would otherwise flood the panel with entries describing a single condition.
  • Continuous sounds start partially elapsed. A continuous instance that should have begun at logical time t0 and unlocks at t1 starts at ACTIVE with its automation tracks and sample-hold ticks advanced to t1 - t0, not from zero. Its procedural draws are consumed in order to reach that position, so the seeded sequence is identical to an unbroken run — reproducibility does not depend on when the listener clicked.
  • The logical clock is not rewound. Audio never causes the performance to replay elapsed time.

If audio is unavailable entirely — no AudioContext, or unlock refused — the performance continues silently. WARN_AUDIO_UNAVAILABLE is raised once, the UI shows the muted state, and no sound instance is created. A missing audio device degrades the exhibit; it does not fail it.

16.9 Pause and resume

Explicit pause and document visibility loss suspend audio along with the rest of the performance (PRD 117). Audio-specific rules:

  • Suspending holds every voice in place. It does not release them, so resuming does not restart a bed of ambience that never stopped being wanted.
  • No catch-up bursts. One-shots that would have been invoked while paused are discarded exactly as at unlock, and report through the same INFO_AUDIO_UNLOCK_SKIP batch.
  • Automation tracks and sample-hold ticks resume from their held logical position; they do not fast-forward through the paused interval.
  • Visibility restoration must not undo an explicit user pause.
  • A pause longer than the runtime's stall bound releases all voices rather than holding them indefinitely, and resuming re-creates continuous sounds at the current logical position by the 16.8 rule. The exact bound is fixed with the GC4 audio lookahead and long-stall policy and is Not yet specified here.

16.10 New diagnostic codes

Added to the section 7 table, which remains the single authoritative list:

Error Code Stage Cause
ERR_AUTOMATION_CONFLICT Semantic More than one automation track directly controls one property in a recipe instance.
ERR_INDETERMINATE_ONESHOT Semantic A oneshot recipe has no computable finite ending; its audible path begins at an unbounded source.
WARN_AUTOMATION_FALLBACK Runtime An exponential automation segment had a zero or sign-crossing endpoint and fell back to linear interpolation.
WARN_VOICE_LIMIT Runtime A voice ceiling was reached; an instance was evicted or a request refused.
WARN_AUDIO_NONFINITE Runtime A non-finite sample reached the master chain and the containing block was muted.
WARN_AUDIO_UNAVAILABLE Runtime No audio device is available; the performance continues silently.

16.11 Required traces before Phase 3c implementation is accepted

Automated, and executable without an audio device:

  1. Each automation mode and each interpolation curve produces its documented value at segment start, midpoint, and end; step holds; smooth has zero slope at both endpoints.
  2. A track holds the first point's value before the first point and the last point's value after the last, and does not loop.
  3. Two tracks on one expanded target are ERR_AUTOMATION_CONFLICT; two modulation routes on that same target still sum.
  4. An exponential segment with a zero endpoint falls back to linear and raises WARN_AUTOMATION_FALLBACK exactly once.
  5. An automation track targeting a property outside the 15.13 registry is ERR_UNSUPPORTED_TARGET; one naming an undeclared node is ERR_INVALID_REFERENCE; one reaching a component's internals is rejected by the 15.15 encapsulation rule.
  6. 65 tracks or 257 total points is ERR_NODE_LIMIT_EXCEEDED; 64 and 256 pass.
  7. Every permitted lifecycle transition is exercised and every forbidden one is rejected; a second stop on a DISPOSED instance is a no-op.
  8. An override masking an automated property releases to the track's current value, not the value held when the override took hold.
  9. The determinable-ending bound matches the 16.5 table for a graph combining impulse, delay, reverb, and resonator; a oneshot fed by an oscillator is ERR_INDETERMINATE_ONESHOT; the same recipe as continuous passes.
  10. At the one-shot ceiling the eviction order of 16.6 is followed, WARN_VOICE_LIMIT is raised, a continuous sound is never evicted by a one-shot request, and a refused request leaves the runtime stable.
  11. Disposal releases every node, connection, automation track, buffer, and subscription; a FINISHED instance still counts against its ceiling until DISPOSED.
  12. A non-finite sample injected at the master chain mutes its block and raises WARN_AUDIO_NONFINITE once.
  13. Unlocking after a delay skips expired one-shots with a single counted INFO_AUDIO_UNLOCK_SKIP, and starts a continuous sound partially elapsed with a procedural sequence identical to an unbroken run.
  14. With no AudioContext available the performance runs silently, raises WARN_AUDIO_UNAVAILABLE once, and creates no instance.

User-observed, and not satisfiable by the above:

  1. The measured digital output peak across worst-case overlapping recipes sits within the ceiling and tolerance of 16.7, on recorded hardware, with the environment details GC6 requires.
  2. Listening observations across the reference exhibits report no clicks, clipping, or pumping on release, eviction, unlock, or pause.
  3. The PRD 129 audio acceptance challenge passes in full.

Traces 15 through 17 close Phase 3 slice 3c-4. Until they do, Phase 3 is not accepted no matter how many automated traces pass.