# Phase 3c slice 2 — audio lifecycle, release, and voice ceilings **Date:** September 5, 2026 **Specification baseline:** Format Specification 0.1 revision 0.4, sections 15-16 **Result:** Implementation slice complete. All 67 tests pass. Two clean builds byte-identical. ## Scope Phase 3c is delivered in four slices. This record covers **slice 3c-2 only**: the audio lifecycle state machine, internal release gain, determinable one-shot endings, voice ceilings, eviction policy, and disposal contract. Slice 3c-2 implements: 1. **The seven-state lifecycle state machine** (16.3): `CREATED`, `SCHEDULED`, `ACTIVE`, `RELEASING`, `FINISHED`, `DISPOSED`, and `FAILED`, with strict enforcement of permitted transitions and rejection of forbidden transitions with `ERR_RUNTIME_FAULT`. Calling `stop()` on `FINISHED`, `DISPOSED`, or `FAILED` is a verified no-op. 2. **Engine-owned internal release gain** (16.4): Placed between sound graph `output` (`sink`) and destination bus (`[output] -> [release gain] -> [bus]`). Initial gain is 1.0. Entering `RELEASING` linearly ramps gain to 0 over `releaseDuration`. Unbypassable, non-addressable by authors, and omitted from author node limits. 3. **Recipe `release` field** (15.16, 16.4): Authorable `release` duration on `audio.recipes.` and `sounds..recipe` within `0ms` to `10s` (default `50ms`). Rejected inside component definitions with `ERR_UNKNOWN_FIELD`. 4. **Determinable one-shot endings** (16.5): Computed via pure longest-path traversal over expanded DAG routes and node contributions (impulse duration, delay decay bound, reverb predelay + decay, resonator longest mode decay, component inlined bound, plus recipe release). An unbounded source (`oscillator` or `noise`) on an audible path in a `oneshot` sound is rejected at validation with `ERR_INDETERMINATE_ONESHOT`. 5. **Voice ceilings and 4-step eviction** (16.6): Independent ceilings for one-shot (`64`) and continuous (`16`) voices. A voice counts against its ceiling from `CREATED` until `DISPOSED`. Eviction policy runs in strict order: - Step 1: Dispose oldest `FINISHED` instance. - Step 2: Advance oldest `RELEASING` instance to immediate completion and dispose. - Step 3 (one-shot only): Evict oldest `ACTIVE` one-shot by starting its release ramp. - Step 4: Otherwise refuse request (continuous or no candidates), maintaining runtime stability. - `WARN_VOICE_LIMIT` is emitted on every eviction and refusal. One-shot and continuous pools are strictly isolated and never evict across pools. 6. **Full disposal** (16.4, 16.6): Releases all Web Audio nodes, connections, buffers, timers, and pool memberships. A `FINISHED` instance continues to occupy its ceiling slot until `DISPOSED`. 7. **Scaffolding removal**: Replaced the fixed 4-second development timer in `src/runtime/app.js` with natural ending management by the engine. ## Specification reconciliation before implementation A pre-implementation review of Section 16 identified 5 inconsistencies, corrected in commit `e5ed468`: 1. Section 15.16 allowed recipe fields table omitted `release`. Added `release` (DurationSpec, default 50ms, optional). 2. Section 16.6 eviction step 2 contradicted the non-hard-stop invariant. Reconciled step 2 to advance release to immediate completion and dispose, rather than immediate drop. 3. Section 16.8 unlock and 16.9 pause discard batching incoherence. Added `INFO_AUDIO_PAUSE_SKIP` to section 7 and 16.10 diagnostics tables. 4. Section 16.5 author remedy text suggested gating an oscillator with an impulse, which is illegal under 15.14 rule 2 (audio route into oscillator is forbidden). Corrected prose to declare `mode: "continuous"` and stop explicitly. 5. Clarified ending bound calculation on expanded graph where component boundaries are already inlined. ## Verification - **Automated test suite**: All 67 tests pass cleanly with zero failures (`npm test`). - **Required traces tested** (16.11): - **Trace 7**: Permitted transitions succeed (`CREATED -> SCHEDULED -> ACTIVE -> RELEASING -> FINISHED -> DISPOSED`, `CREATED -> FINISHED`, `SCHEDULED -> RELEASING`, `ACTIVE -> FINISHED`, `* -> FAILED -> [terminal]`). Forbidden transitions throw `ERR_RUNTIME_FAULT`. Second stop on terminal states is idempotent. - **Trace 9**: Determinable ending bounds match the section 16.5 contribution table across impulse, resonator, delay (with and without feedback), reverb, branching mixers, and component expansion. Unbounded oscillator/noise in a oneshot recipe is rejected with `ERR_INDETERMINATE_ONESHOT`, while continuous sounds with identical shapes validate cleanly. - **Trace 10**: Eviction order 1 -> 2 -> 3 -> 4 is verified at ceiling. `WARN_VOICE_LIMIT` is emitted with sound and ceiling metadata. Continuous sound is never evicted by one-shot request, and refused request leaves runtime stable. - **Trace 11**: Disposal releases every node and connection. A `FINISHED` instance still occupies its ceiling budget until `DISPOSED`. - **Recipe `release` validation**: Validates `0ms` to `10s`, rejects out-of-bounds (`11s`), invalid duration strings (`-10ms`, `slow`), and rejects `release` declared inside components (`ERR_UNKNOWN_FIELD`). - **Exhibit validation**: All three exhibits (`minimal-audio.xzbt`, `minimal-fixed.xzbt`, `minimal-random.xzbt`) validate cleanly with 0 errors via `tools/validate-exhibit.mjs`. - **Deterministic build**: `node tools/build-xzbt.mjs` runs twice with identical digest: `SHA-256 d7d16a89f9b1a9e21262232382d7ae53a0a84abafce191a873327e66dc4103a6` (172,825 bytes). ## Not established by this record - **No automation tracks**: Automation tracks and modes (16.1, 16.2) belong to slice 3c-3. - **No master protection measurement**: Master output protection (16.7) verification belongs to slice 3c-4 under GC6. - **No sound has been heard from any build**: Automated tests verify Web Audio node topologies, connections, and state transitions against headless stand-ins. Real audible playback observations remain open. - **Phase 3 is not accepted**: Slices 3c-3 and 3c-4 remain before Phase 3 can be submitted for acceptance.