6.0 KiB
6.0 KiB
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:
- The seven-state lifecycle state machine (16.3):
CREATED,SCHEDULED,ACTIVE,RELEASING,FINISHED,DISPOSED, andFAILED, with strict enforcement of permitted transitions and rejection of forbidden transitions withERR_RUNTIME_FAULT. Callingstop()onFINISHED,DISPOSED, orFAILEDis a verified no-op. - 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. EnteringRELEASINGlinearly ramps gain to 0 overreleaseDuration. Unbypassable, non-addressable by authors, and omitted from author node limits. - Recipe
releasefield (15.16, 16.4): Authorablereleaseduration onaudio.recipes.<id>andsounds.<id>.recipewithin0msto10s(default50ms). Rejected inside component definitions withERR_UNKNOWN_FIELD. - 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 (
oscillatorornoise) on an audible path in aoneshotsound is rejected at validation withERR_INDETERMINATE_ONESHOT. - Voice ceilings and 4-step eviction (16.6): Independent ceilings for one-shot (
64) and continuous (16) voices. A voice counts against its ceiling fromCREATEDuntilDISPOSED. Eviction policy runs in strict order:- Step 1: Dispose oldest
FINISHEDinstance. - Step 2: Advance oldest
RELEASINGinstance to immediate completion and dispose. - Step 3 (one-shot only): Evict oldest
ACTIVEone-shot by starting its release ramp. - Step 4: Otherwise refuse request (continuous or no candidates), maintaining runtime stability.
WARN_VOICE_LIMITis emitted on every eviction and refusal. One-shot and continuous pools are strictly isolated and never evict across pools.
- Step 1: Dispose oldest
- Full disposal (16.4, 16.6): Releases all Web Audio nodes, connections, buffers, timers, and pool memberships. A
FINISHEDinstance continues to occupy its ceiling slot untilDISPOSED. - Scaffolding removal: Replaced the fixed 4-second development timer in
src/runtime/app.jswith natural ending management by the engine.
Specification reconciliation before implementation
A pre-implementation review of Section 16 identified 5 inconsistencies, corrected in commit e5ed468:
- Section 15.16 allowed recipe fields table omitted
release. Addedrelease(DurationSpec, default 50ms, optional). - 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.
- Section 16.8 unlock and 16.9 pause discard batching incoherence. Added
INFO_AUDIO_PAUSE_SKIPto section 7 and 16.10 diagnostics tables. - 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. - 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 throwERR_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_LIMITis 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
FINISHEDinstance still occupies its ceiling budget untilDISPOSED. - Recipe
releasevalidation: Validates0msto10s, rejects out-of-bounds (11s), invalid duration strings (-10ms,slow), and rejectsreleasedeclared inside components (ERR_UNKNOWN_FIELD).
- Trace 7: Permitted transitions succeed (
- Exhibit validation: All three exhibits (
minimal-audio.xzbt,minimal-fixed.xzbt,minimal-random.xzbt) validate cleanly with 0 errors viatools/validate-exhibit.mjs. - Deterministic build:
node tools/build-xzbt.mjsruns 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.