|
|
|
@@ -1,8 +1,8 @@
|
|
|
|
|
# XZBT Format Specification 0.1
|
|
|
|
|
|
|
|
|
|
**XZBT format version:** 0.1
|
|
|
|
|
**Document revision:** 0.3
|
|
|
|
|
**Status:** Normative specification for shared contracts (document, types, ValueSpec, ConditionSpec, resolution, bindings, and transitions); subsystem contracts in progress
|
|
|
|
|
**Document revision:** 0.4
|
|
|
|
|
**Status:** Normative specification for shared contracts (document, types, ValueSpec, ConditionSpec, resolution, bindings, and transitions); the Audio subsystem authoring contract (Phase 3a sources/control sources, Phase 3b processing/routing/components/buses) is complete; audio automation precedence, lifecycle, and master protection (Phase 3c) and the remaining subsystem contracts are in progress
|
|
|
|
|
**Related resources:** [PRD](../XZBT_0-1_MVP_Product_Requirements_Document.md), [decisions](XZBT_0-1_Gap_Closure_Decisions.md), [verification](XZBT_0-1_Verification_Gates.md)
|
|
|
|
|
|
|
|
|
|
This document defines normative syntax and runtime semantics for XZBT 0.1 exhibits. An exhibit is a UTF-8 JSON document that configures generic procedural visual, audio, cadence, and orchestration primitives. It does not contain executable JavaScript.
|
|
|
|
@@ -280,6 +280,13 @@ To ensure consistent error reporting between structural schema validation, seman
|
|
|
|
|
| `WARN_CLOCK_STALL` | Runtime | Elapsed wall time or accumulated work exceeded the fixed-step per-turn limits and was discarded. |
|
|
|
|
|
| `WARN_CLEANUP_FORCED` | Runtime | A cleanup owner reached its deadline and force-disposed remaining resources. |
|
|
|
|
|
| `INFO_AUDIO_UNLOCK_SKIP` | Runtime | One or more pre-unlock one-shots were intentionally not replayed. |
|
|
|
|
|
| `ERR_INVALID_NODE_TYPE` | Semantic | Audio graph node `type` is not a member of Audio Graph Node Set 0.1. |
|
|
|
|
|
| `ERR_NODE_LIMIT_EXCEEDED` | Semantic | An authoring limit is exceeded (oscillator partials, resonator modes, expanded nodes or routes per sound). |
|
|
|
|
|
| `ERR_INVALID_RANGE_ORDER` | Semantic | Declared paired bounds (e.g. `sample-hold` `min`/`max`) are not in strictly increasing order after resolution. |
|
|
|
|
|
| `ERR_INVALID_ROUTE` | Semantic | An audio or modulation route is structurally resolvable but illegal under the audio graph legality rules. |
|
|
|
|
|
| `ERR_NO_AUDIBLE_PATH` | Semantic | A sound's expanded audio graph has no chain of audio routes from a non-control source to `output`. |
|
|
|
|
|
| `ERR_COMPONENT_RECURSION` | Semantic | An audio component instantiates itself transitively, or component nesting exceeds 8 levels. |
|
|
|
|
|
| `WARN_AUDIO_RATE_CLAMP` | Runtime | A frequency field was clamped to the device's `audioMaxFrequency` at node instantiation. |
|
|
|
|
|
|
|
|
|
|
## 8. Shared value resolution, bindings, and transitions
|
|
|
|
|
|
|
|
|
@@ -498,7 +505,7 @@ Complete shared contracts before implementing dependent subsystems. Use PRD sect
|
|
|
|
|
| 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 | Subsystem contract (Phase 3) |
|
|
|
|
|
| Audio | 34-60 | Complete recipe/component shapes, node defaults, automation timing, clamp implementation, one-shot endings and tails, unlock behavior, master protection contract | **Authoring contract complete (Rev 0.4 / Phase 3a-3b):** units, graph objects, all sixteen node types, routing, modulation, graph legality, authoring limits, components, sounds/recipes, and buses. Automation precedence (54), lifecycle states (57), unlock behavior, and runtime safety/master protection (58) remain (Phase 3c) |
|
|
|
|
|
| Cadence | 61-68 | Exact selection/cooldown/overlap fields, clocks, pool collisions, pending audio, manual sampling isolation and continuous-sample termination | Subsystem contract (Phase 5) |
|
|
|
|
|
| Visuals | 69-89 | Primitive/system/behavior fields, angle and motion units, transform order, depth projection, field behavior, morph compatibility, effect approximation and limits | Subsystem contract (Phase 4) |
|
|
|
|
|
| Events/scenarios | 90-102 | Shared ownership, hooks/failure ordering, condition rearming, deferred ordering/expiry, dispatch limits; full trigger/timeline shapes remain for Phase 6 | **Shared lifecycle contract complete (Rev 0.3 / GC5)** |
|
|
|
|
@@ -541,3 +548,697 @@ On successful candidate activation, atomically commit the new definition, reconc
|
|
|
|
|
Development uses separate source modules. The build must have pinned tool versions and inputs, stable module/asset order, no wall-clock timestamps or absolute paths in output, and a documented single command that produces `XZBT.html`. Two clean builds from the same revision must have identical SHA-256 digests. The artifact must contain all runtime code/assets/fonts and pass direct-file offline verification with no external runtime requests.
|
|
|
|
|
|
|
|
|
|
Structural JSON Schema does not replace semantic validation. The internal schema and fixtures belong to 0.1 implementation work; public schema distribution and editor integration may follow later.
|
|
|
|
|
|
|
|
|
|
## 14. Audio Subsystem Contract — Sources and Control Sources (Phase 3a)
|
|
|
|
|
|
|
|
|
|
This section opens the Audio contract required by section 11 (PRD 34-60). It covers the audio pipeline overview, canonical units, the shared audio-graph object shape, node-field resolution scope, the audio frequency ceiling, audio reproducibility scope, and the six source and control-source node types (PRD 36-42).
|
|
|
|
|
|
|
|
|
|
Processing nodes, routing and modulation, graph legality, components, sound definitions, and buses (PRD 43-53, 55, 56, 59, 60) are specified in section 15 (Phase 3b). Audio automation precedence (PRD 54), the full runtime lifecycle state machine (PRD 57), and the consolidated safety-limit and master-protection contract (PRD 58) are **Not yet specified** and arrive in Phase 3c.
|
|
|
|
|
|
|
|
|
|
### 14.1 Pipeline and scope
|
|
|
|
|
|
|
|
|
|
The audio pipeline is, in order: audio primitives (14.7-14.12) → audio graphs (14.3) → reusable `components.audio.*` components (15.15) → sound recipes (15.16) → sound instances → declared buses (15.17) → bus processing → engine master protection → output (PRD 34).
|
|
|
|
|
|
|
|
|
|
XZBT never implements a node, field, or function named after a specific exhibit's thematic sound. Every construct in this contract is generic.
|
|
|
|
|
|
|
|
|
|
### 14.2 Canonical units
|
|
|
|
|
|
|
|
|
|
| Quantity | Unit | Notes |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| frequency | Hz | Bounded above by `audioMaxFrequency` (14.5). |
|
|
|
|
|
| detune | cents | `-4800` to `+4800` unless a node contract states otherwise. |
|
|
|
|
|
| gain / amplitude | linear scalar | Not dB unless the field's contract says dB. |
|
|
|
|
|
| filter and compressor gain | dB | Applies to `filter.gain`, `compressor.threshold`, and `compressor.knee` (15.3, 15.4). |
|
|
|
|
|
| Q | unitless | Applies to `filter.q` (15.3). |
|
|
|
|
|
| normalized controls | `0` to `1` | Polarity-free control ranges (impulse `amplitude`, `mix`, `damping`, waveshaper `amount`). |
|
|
|
|
|
| pan | `-1` to `+1` | `-1` full left, `0` center, `+1` full right (15.8). |
|
|
|
|
|
| time | DurationSpec 0.1 | Section 6; a procedural `TimeSpec` is permitted wherever a field's contract says DurationSpec. |
|
|
|
|
|
| modulation depth | target property's unit | Section 15.13 (PRD 53). |
|
|
|
|
|
|
|
|
|
|
### 14.3 Audio graph objects
|
|
|
|
|
|
|
|
|
|
An **audio graph object** is the shared container for every audio node network in an exhibit:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"nodes": {
|
|
|
|
|
"tone": { "type": "oscillator", "frequency": 220 },
|
|
|
|
|
"level": { "type": "gain", "gain": 0.4 }
|
|
|
|
|
},
|
|
|
|
|
"routes": [
|
|
|
|
|
{ "from": "tone", "to": "level" },
|
|
|
|
|
{ "from": "level", "to": "output" }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Required | Notes |
|
|
|
|
|
| --- | --- | :---: | --- |
|
|
|
|
|
| `nodes` | object keyed by node ID | Yes | Each value is a node object (14.4). An empty `nodes` object is `ERR_SCHEMA_VALIDATION`. |
|
|
|
|
|
| `routes` | array | No (defaults to `[]`) | Route objects, specified in 15.12-15.13. A graph object with no route reaching `output` is `ERR_NO_AUDIBLE_PATH` unless it is a component graph, which terminates at its declared component output instead (15.15). |
|
|
|
|
|
|
|
|
|
|
Audio graph objects are embedded at exactly three places, whose surrounding fields are specified in Phase 3b:
|
|
|
|
|
|
|
|
|
|
* `audio.recipes.<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 a separate Phase 3c stage and are **Not yet specified**; an `automation` field on any node or recipe is `ERR_UNKNOWN_FIELD` until that contract lands.
|
|
|
|
|
|
|
|
|
|
### 14.5 Audio frequency ceiling and validation staging
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
audioMaxFrequency = min(24000, sampleRate x 0.45)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`sampleRate` is the live `AudioContext.sampleRate`, which is a property of the playback device and is unknown while an exhibit is being imported or validated. The ceiling is therefore enforced in two tiers, and an implementation must not conflate them:
|
|
|
|
|
|
|
|
|
|
1. **Semantic stage (import, refresh, and validation tooling).** A frequency field whose authored value resolves to a literal outside its declared range, checked against the device-independent ceiling `24000`, is `ERR_OUT_OF_BOUNDS`. Validation never opens an `AudioContext` and never depends on audio hardware.
|
|
|
|
|
2. **Instantiation stage (node creation on a live context).** The runtime computes `audioMaxFrequency` from the actual `AudioContext.sampleRate` and **clamps** the resolved value into range. Clamping, not failing, is required: a cached exhibit authored on a 48 kHz device must still activate on a 44.1 kHz device. Each clamped node field raises `WARN_AUDIO_RATE_CLAMP` once per node instance.
|
|
|
|
|
|
|
|
|
|
A hardcoded `24000` at instantiation is a contract violation even though it is the correct semantic-stage ceiling.
|
|
|
|
|
|
|
|
|
|
### 14.6 Reproducibility scope for audio sources
|
|
|
|
|
|
|
|
|
|
Section 9.3 promises identical *procedural decisions*, not identical floating-point audio samples. This contract fixes where that line falls for audio sources:
|
|
|
|
|
|
|
|
|
|
* **`noise` and `impulse` sample generation consumes no XZBT procedural stream.** Their per-sample output comes from the audio implementation's own generator and is not reproducible across runs or devices. It cannot perturb any seeded stream, because it draws from none.
|
|
|
|
|
* **`sample-hold` does draw from a seeded stream.** Its held values are observable control decisions that can drive audible outcomes, so they are reproducible. It uses domain `sound` with the documented child key defined in 14.12.
|
|
|
|
|
* **Every other source and control-source field** is a ValueSpec resolved once at instantiation from the sound instance's stream (14.4), and is therefore reproducible.
|
|
|
|
|
|
|
|
|
|
Rendering and audio callbacks consume no procedural stream (section 9.3); a `sample-hold` tick is a logical decision, not a render step, and its draws occur on the node instance's own stream in tick order.
|
|
|
|
|
|
|
|
|
|
### 14.7 Oscillator (`oscillator`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"type": "oscillator",
|
|
|
|
|
"waveform": "sine",
|
|
|
|
|
"frequency": 440,
|
|
|
|
|
"detune": 0
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec |
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
| `waveform` | enum | `sine`, `triangle`, `square`, `sawtooth`, `custom` | `sine` | No — literal enum only. |
|
|
|
|
|
| `frequency` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "type": "noise", "color": "pink" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec |
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
| `color` | enum | `white`, `pink`, `brown` | `white` | No — literal enum only; `color` selects a fixed spectral shape, not a continuously varying parameter. |
|
|
|
|
|
|
|
|
|
|
Spectral shapes, measured over `20` Hz to `20000` Hz:
|
|
|
|
|
|
|
|
|
|
| `color` | Power spectral slope | Normalization |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| `white` | `0` dB/octave (flat) | Unit RMS |
|
|
|
|
|
| `pink` | `-3` dB/octave | Unit RMS |
|
|
|
|
|
| `brown` | `-6` dB/octave | Unit RMS |
|
|
|
|
|
|
|
|
|
|
The filter used to realize `pink` and `brown` is runtime-defined; its response must track the declared slope within `±3` dB across `20` Hz to `20000` Hz. All three colors are normalized to equal RMS so that changing `color` does not change perceived level.
|
|
|
|
|
|
|
|
|
|
A `noise` node has no built-in volume control; author a downstream `gain` node (15.2) for level.
|
|
|
|
|
|
|
|
|
|
**Minimal example:** `{ "type": "noise" }`. **Invalid case:** `{ "type": "noise", "color": "grey" }` → `ERR_TYPE_MISMATCH`.
|
|
|
|
|
|
|
|
|
|
### 14.9 Impulse (`impulse`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"type": "impulse",
|
|
|
|
|
"color": "white",
|
|
|
|
|
"duration": "10ms",
|
|
|
|
|
"amplitude": 1,
|
|
|
|
|
"decay": "exponential"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec |
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
| `color` | enum | `white`, `pink`, `brown` | `white` | No. Spectral shapes are those of 14.8. |
|
|
|
|
|
| `duration` | DurationSpec 0.1 | `1ms` to `500ms` | `10ms` | Sampled once at instantiation (14.4). |
|
|
|
|
|
| `amplitude` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "type": "constant", "value": 1 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Control-only source (PRD 40). A `constant` may modulate numeric properties through a modulation route (15.13) but may never reach `output`, directly or through any chain; such a route is `ERR_INVALID_ROUTE` under graph legality (15.14).
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec |
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
| `value` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"type": "lfo",
|
|
|
|
|
"waveform": "sine",
|
|
|
|
|
"frequency": 1,
|
|
|
|
|
"amplitude": 1,
|
|
|
|
|
"polarity": "bipolar",
|
|
|
|
|
"phase": 0
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Control-only source (PRD 41). Audio-rate modulation uses an ordinary `oscillator` instead. An `lfo` reaching `output` through any chain is `ERR_INVALID_ROUTE` (15.14).
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec |
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
| `waveform` | enum | `sine`, `triangle`, `square`, `sawtooth` | `sine` | No. `custom` is oscillator-only and is `ERR_TYPE_MISMATCH` here. |
|
|
|
|
|
| `frequency` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"type": "sample-hold",
|
|
|
|
|
"rate": 2,
|
|
|
|
|
"min": -1,
|
|
|
|
|
"max": 1,
|
|
|
|
|
"slew": "0ms"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Control-only source (PRD 42). A `sample-hold` reaching `output` through any chain is `ERR_INVALID_ROUTE` (15.14). See 14.11 for the distinction from a top-level `modulators.<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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
<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.
|
|
|
|
|
|
|
|
|
|
Audio automation precedence (PRD 54), the runtime lifecycle state machine (PRD 57), and the runtime half of the safety-limit and master-protection contract (PRD 58 voice ceilings, peak ceiling, numerical tolerance, release behavior, finite-sample handling) remain **Not yet specified** and arrive in Phase 3c.
|
|
|
|
|
|
|
|
|
|
### 15.1 Processing and routing node set
|
|
|
|
|
|
|
|
|
|
| `type` | Class | Audio inputs | Audio output | Section |
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
| `gain` | Processing | Many (summed) | Yes | 15.2 |
|
|
|
|
|
| `filter` | Processing | Many (summed) | Yes | 15.3 |
|
|
|
|
|
| `compressor` | Processing | Many (summed) | Yes | 15.4 |
|
|
|
|
|
| `waveshaper` | Processing | Many (summed) | Yes | 15.5 |
|
|
|
|
|
| `delay` | Processing | Many (summed) | Yes | 15.6 |
|
|
|
|
|
| `reverb` | Processing | Many (summed) | Yes | 15.7 |
|
|
|
|
|
| `stereo-pan` | Processing | Many (summed) | Yes | 15.8 |
|
|
|
|
|
| `mixer` | Routing | Many (summed) | Yes | 15.9 |
|
|
|
|
|
| `resonator` | Processing | Many (summed) | Yes | 15.10 |
|
|
|
|
|
| `component` | Composite | Zero or one, per the component's `input` declaration | Yes | 15.11 |
|
|
|
|
|
|
|
|
|
|
Every node that accepts audio input accepts any number of incoming audio routes and sums them; there is no per-input index in 0.1. Gain staging is explicit and uses `gain` nodes (PRD 50).
|
|
|
|
|
|
|
|
|
|
### 15.2 Gain (`gain`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "type": "gain", "gain": 1 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec | Modulatable |
|
|
|
|
|
| --- | --- | --- | --- | --- | :---: |
|
|
|
|
|
| `gain` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "type": "filter", "mode": "lowpass", "frequency": 800, "q": 0.7 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec | Modulatable |
|
|
|
|
|
| --- | --- | --- | --- | --- | :---: |
|
|
|
|
|
| `mode` | enum | `lowpass`, `highpass`, `bandpass`, `notch`, `peaking`, `lowshelf`, `highshelf`, `allpass` | `lowpass` | No | No |
|
|
|
|
|
| `frequency` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "type": "waveshaper", "shape": "soft-clip", "amount": 0.5, "oversample": "2x" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec | Modulatable |
|
|
|
|
|
| --- | --- | --- | --- | --- | :---: |
|
|
|
|
|
| `shape` | enum | `soft-clip`, `hard-clip`, `saturation` | `soft-clip` | No | No |
|
|
|
|
|
| `amount` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "type": "delay", "time": "250ms", "feedback": 0.2, "mix": 0.5 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec | Modulatable |
|
|
|
|
|
| --- | --- | --- | --- | --- | :---: |
|
|
|
|
|
| `time` | DurationSpec 0.1 | `0ms` to `10s` | `250ms` | Sampled once | Yes, milliseconds |
|
|
|
|
|
| `feedback` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "type": "mixer" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A mixer accepts any number of audio inputs and exposes one audio output. It has no fields and no gain controls in 0.1; gain staging uses explicit `gain` nodes (PRD 50). Any property on a mixer node other than `type` is `ERR_UNKNOWN_FIELD`.
|
|
|
|
|
|
|
|
|
|
A mixer is a convenience for graph readability: because every audio-accepting node already sums its inputs (15.1), a mixer is never required for correctness.
|
|
|
|
|
|
|
|
|
|
### 15.10 Resonator (`resonator`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"type": "resonator",
|
|
|
|
|
"fundamental": 120,
|
|
|
|
|
"modes": [
|
|
|
|
|
{ "ratio": 1, "gain": 1, "decay": "1.2s" },
|
|
|
|
|
{ "ratio": 2.7, "gain": 0.4, "decay": "800ms" }
|
|
|
|
|
],
|
|
|
|
|
"mix": 1
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Range | Default | ValueSpec | Modulatable |
|
|
|
|
|
| --- | --- | --- | --- | --- | :---: |
|
|
|
|
|
| `fundamental` | ValueSpec\<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`)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "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):
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "from": "tone", "to": "filter" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "from": "vibrato", "to": "tone.frequency", "depth": 18 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Field | Type | Required | Notes |
|
|
|
|
|
| --- | --- | :---: | --- |
|
|
|
|
|
| `from` | string | Yes | A node key in the same graph, or the reserved `input` inside a component graph that declares `input: true`. `output` may never be a `from` (`ERR_INVALID_ROUTE`). |
|
|
|
|
|
| `to` | string | Yes | Audio route: a node key in the same graph, or the reserved `output`. Modulation route: `<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`:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
value = clamp(b + sum(s_i x d_i), property minimum, property maximum)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Multiple legal modulation routes are summed (PRD 53). The clamp uses the property's declared range from its node contract and is the audio application of the safety-clamp stage of section 8.1; it is not a separate precedence system. A control source's output is its own value in its own units before scaling: an `lfo` with `amplitude: 1` and `polarity: "bipolar"` contributes `[-d, +d]` to its target.
|
|
|
|
|
|
|
|
|
|
Automation (PRD 54) sits between binding and override in the shared pipeline and is **Not yet specified**; in Phase 3b the resolution of a node property is exactly base, then modulation sum, then safety clamp.
|
|
|
|
|
|
|
|
|
|
### 15.14 Graph legality and authoring limits
|
|
|
|
|
|
|
|
|
|
The final expanded audio graph of a sound — the recipe graph with every component instance inlined — must satisfy all of the following (PRD 55). Each check names the diagnostic it emits.
|
|
|
|
|
|
|
|
|
|
| # | Rule | Diagnostic |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| 1 | Every node key matches the identifier rule and is not `output` | `ERR_INVALID_ID` |
|
|
|
|
|
| 2 | Every node `type` is in Audio Graph Node Set 0.1 | `ERR_INVALID_NODE_TYPE` |
|
|
|
|
|
| 3 | Every route `from` and `to` resolves to a declared node, `output`, or a legal `input` | `ERR_INVALID_REFERENCE` |
|
|
|
|
|
| 4 | `output` is sink-only and never a route `from` | `ERR_INVALID_ROUTE` |
|
|
|
|
|
| 5 | A control source (`constant`, `lfo`, `sample-hold`) never reaches `output` through any chain of audio routes | `ERR_INVALID_ROUTE` |
|
|
|
|
|
| 6 | A source node (`oscillator`, `noise`, `impulse`, and every control source) never receives an audio route | `ERR_INVALID_ROUTE` |
|
|
|
|
|
| 7 | The audio route graph is acyclic | `ERR_CYCLIC_DEPENDENCY` |
|
|
|
|
|
| 8 | The modulation dependency graph is acyclic | `ERR_CYCLIC_DEPENDENCY` |
|
|
|
|
|
| 9 | At least one audio route chain from a non-control source reaches `output` | `ERR_NO_AUDIBLE_PATH` |
|
|
|
|
|
| 10 | A component never instantiates itself, directly or transitively | `ERR_COMPONENT_RECURSION` |
|
|
|
|
|
| 11 | Component nesting depth is at most `8` | `ERR_COMPONENT_RECURSION` |
|
|
|
|
|
| 12 | Graph limits are respected | `ERR_NODE_LIMIT_EXCEEDED` |
|
|
|
|
|
|
|
|
|
|
Rule 5 is the enforcement point for the control-only status asserted in 14.10, 14.11, and 14.12. Rule 6 makes source nodes true graph roots. Rule 7 is what forbids unrestricted author-built feedback (PRD 47); a `delay` node's internal feedback path is not part of the route graph and does not violate it.
|
|
|
|
|
|
|
|
|
|
The authoring-time limits enforced in Phase 3b are the subset of PRD 58 that a document can violate on its own:
|
|
|
|
|
|
|
|
|
|
| Limit | Value |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| Expanded nodes per sound | `128` |
|
|
|
|
|
| Routes per sound | `256` |
|
|
|
|
|
| Component nesting depth | `8` |
|
|
|
|
|
| Resonator modes | `16` |
|
|
|
|
|
| Custom oscillator partials | `64` |
|
|
|
|
|
|
|
|
|
|
The remaining PRD 58 limits — approximate one-shot voices (`64`), approximate continuous sounds (`16`), automation tracks (`64`), and automation points (`256`) — are runtime ceilings rather than document properties and belong to Phase 3c with the lifecycle contract. Master protection likewise remains Phase 3c: this section does not establish that the protection contract passes, and the presence of a compressor in a graph never constitutes master protection (PRD 58).
|
|
|
|
|
|
|
|
|
|
### 15.15 Audio components
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"components": {
|
|
|
|
|
"audio": {
|
|
|
|
|
"voice": {
|
|
|
|
|
"parameters": {
|
|
|
|
|
"pitch": { "type": "number", "default": 220, "min": 20, "max": 2000 }
|
|
|
|
|
},
|
|
|
|
|
"input": false,
|
|
|
|
|
"nodes": {
|
|
|
|
|
"tone": { "type": "oscillator", "frequency": { "ref": "inputs.pitch" } },
|
|
|
|
|
"level": { "type": "gain", "gain": 0.3 }
|
|
|
|
|
},
|
|
|
|
|
"routes": [
|
|
|
|
|
{ "from": "tone", "to": "level" },
|
|
|
|
|
{ "from": "level", "to": "output" }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A `components.audio.<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
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"sounds": {
|
|
|
|
|
"relay-click": {
|
|
|
|
|
"name": "Relay Click",
|
|
|
|
|
"tags": ["mechanical", "electrical"],
|
|
|
|
|
"usage": ["automatic", "manual", "scenario"],
|
|
|
|
|
"cadence": { "class": "routine" },
|
|
|
|
|
"bus": "effects",
|
|
|
|
|
"recipe": {
|
|
|
|
|
"mode": "oneshot",
|
|
|
|
|
"nodes": { "hit": { "type": "impulse" }, "body": { "type": "resonator", "modes": [{ "ratio": 1 }] } },
|
|
|
|
|
"routes": [{ "from": "hit", "to": "body" }, { "from": "body", "to": "output" }]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A `sounds.<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
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"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.
|
|
|
|
|