Files
XZBT/docs/evidence/phase3/2026-09-05-phase3c-lifecycle-voices.md
T

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:

  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.