feat(audio): implement phase 3c slice 2 lifecycle and voices
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# 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.<id>` and `sounds.<id>.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.
|
||||
Reference in New Issue
Block a user