Files
Portfolio/docs/superpowers/plans/2026-09-04-portfolio-adaptive-soundtrack.md

165 KiB

Portfolio Adaptive Soundtrack Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Preserve the approved authored Nightdrive score while adding stable four-mood chapter shading, a restrained acceleration response, a synchronized drive-lift layer, and an indefinite seamless afterglow without jarring scroll-triggered transitions.

Architecture: Keep one gesture-created AudioContext, one uninterrupted musical clock, and the existing intro/main recordings. A pure audio-intent coordinator stabilizes responsive journey geometry and navigation epochs before issuing separate chapter and drive commands; the soundtrack engine routes the authored bed, short synchronized overlays, and a procedural drive texture through a conservative adaptive mixer. New music is produced and listener-approved outside the Portfolio repository before its hash-bound web derivatives enter the existing prerender pipeline.

Tech Stack: Vue 3, TypeScript 5.9, Vite 8, Web Audio API, Zod 4, Vitest 4, Playwright 1.62, FFmpeg/FFprobe, Python 3 with NumPy/SoundFile, ACE-Step 1.5 XL-SFT with the 4B planner

Spec: docs/superpowers/specs/2026-09-04-portfolio-adaptive-soundtrack-design.md

Global Constraints

  • Work only on branch codex/portfolio-audio-refinement in /home/luca/Documents/Portfolio/.worktrees/portfolio-audio-refinement for Portfolio changes.
  • Keep candidates, WAV masters, measurements, prompts, seeds, and audition renders under /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1 until the user approves both additions.
  • Preserve the approved 86 BPM, F minor, 4/4, 64-bar main master unchanged as the dominant source.
  • Preserve intro-in -> intro-idle-first-pass -> intro-idle-loop -> intro-out -> main -> afterglow-in -> afterglow-idle-loop.
  • Keep the main score running through chapter changes and returns to Start; never seek, restart, pitch-shift, time-stretch, or change its playback rate.
  • Use exactly four moods: open, shadow, lift, and resolve, with a 1.4-second mix ramp.
  • Require 6vh boundary hysteresis and 400 ms dwell for passive musical chapter changes.
  • Treat explicit navigation as latest-wins, commit only the current settled epoch, and cap its drive energy at 0.35.
  • Apply an audio dead zone of 0.08, a 220 ms drive-texture attack, and a 900 ms release.
  • Arm drive lift only for physical energy above 0.62 for 450 ms; arm its exit below 0.32 for 800 ms; use one-beat fades.
  • Emit drive energy at most once per 33 ms and only after a delta of at least 0.02, except dead-zone, threshold, source, and final-zero crossings.
  • Use one four-bar drive-lift loop of exactly 535,814 samples and one afterglow runtime of exactly 1,205,581 samples at 48 kHz.
  • Keep the afterglow transition in samples [0, 133953) and loop only [133953, 1205581); never repeat the transition bar.
  • Keep engine/road texture barely audible at cruise and subordinate to the authored score at maximum acceleration.
  • Retain explicit gesture gating, one-context ownership, mute, volume, visibility, interruption, BFCache, authenticated F10-F13 fault hooks, graceful optional-layer failure, and idempotent teardown.
  • Keep the exact asset-reference closure and Opus-first/MP3-fallback contract. Do not reduce intro/main quality. Deferred audio must remain at or below 16 MiB.
  • Do not restore music-sequencer.ts, the old combined procedural synth graph, full-length stems, a dynamics compressor across the master, or a new settings interface.
  • Run only the focused checks named in each task. Run the complete compiled-preview/browser gate once at the end rather than repeating it between tasks.
  • Treat analyzer and loudness measurements as technical screening only. Musical approval is a human listening decision.

File Structure

External production workspace

  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/references/: fixed approved-master excerpts used for conditioning.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/: four fixed-seed raw renders and two exact afterglow runtimes.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/: exact eight-bar afterglow-idle and four-bar drive-lift loops.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/: decoded derivatives and three-repeat seam renders.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/auditions/: in-context A/B listening files.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/generation/: ACE-Step task records.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analysis/: signal, loudness, silence, seam, checksum, and byte reports.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/: listener-approved Opus and MP3 derivatives.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh: persistent analyzer/FFmpeg screening wrapper reused across production steps.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-afterglow-runtime.py: exact entry-preserving transition/runtime assembler.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-production-manifest.mjs: one-use deterministic manifest assembler fed by the approved seed pair and saved reports.
  • /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/production-manifest.json: reproducible source, selection, loop, measurement, and delivery record.

New Portfolio modules

  • src/audio/audio-intent-coordinator.ts: pure passive-hysteresis, explicit-settlement, generation, drive classification, and emission-rate state machine.
  • src/audio/drive-texture.ts: deterministic low engine, stereo road/air texture, ignition envelope, smoothing, and exact-once disposal.
  • src/audio/adaptive-mixer.ts: fixed-headroom bed routing, four mood profiles, and beat-aligned drive-lift gate.
  • src/composables/useAudioIntent.ts: shared-frame Vue bridge from journey/navigation snapshots to chapter and drive refs.
  • tests/helpers/fake-audio-context.ts: call-recording Web Audio fake used only by focused mixer/engine/texture tests.
  • tests/unit/audio-intent-coordinator.test.ts: stable chapter and drive emission contract.
  • tests/unit/use-audio-intent.test.ts: first measured deep-link seeding and visibility suppression.
  • tests/unit/drive-texture.test.ts: node reuse, attack/release automation, deterministic stereo creation, and disposal.
  • tests/unit/adaptive-mixer.test.ts: mood ramps and sustained physical drive-lift gating.
  • tests/unit/soundtrack-engine.test.ts: cancellable intro departure, shared clock, optional loading, afterglow, and cleanup.

Existing Portfolio modules with changed responsibilities

  • src/journey/navigation-controller.ts: add idle | travelling | settled phase to every navigation epoch.
  • src/journey/types.ts: carry activation-relative geometry and layout revision in JourneySnapshot.
  • src/journey/section-controller.ts: derive normalized active/next-heading boundary offsets without a second DOM read.
  • src/composables/useJourneySections.ts: expose its measured snapshot, frame request, and post-measurement frame subscription as one binding.
  • src/audio/journey-music-state.ts: map canonical sections to typed ChapterIntent values.
  • src/audio/types.ts: replace combined journey state with independent chapter and drive commands; add afterglow and drive-lift sources.
  • src/audio/create-audio-controller.ts: cache, normalize, and forward independent pre-activation command values.
  • src/audio/soundtrack-transport.ts: own exact beat/bar/sample constants and phase/offset helpers.
  • src/audio/soundtrack-engine.ts: orchestrate dominant and optional buffer loading, cancellable intro scheduling, one main anchor, afterglow, mixer, texture, and teardown.
  • src/composables/useAudioEngine.ts: watch chapter and drive refs independently without recreating the controller.
  • src/components/AppShell.vue: remove eager pendingAudioDestination, construct the audio-intent binding, and resolve all four soundtrack regions.
  • src/content/types.ts, src/content/schemas.ts, content/public/portfolio-content.json: publish four strict soundtrack regions.
  • content/public/asset-manifest.json, inputs/prepared-assets/: add four hash-bound approved derivatives.
  • scripts/materialize-empty-asset-set.mjs: include all four soundtrack regions in exact reference closure.
  • scripts/lib/bundle-budgets.mjs, scripts/lib/bundle-budgets.d.mts, scripts/check-bundle-budgets.mjs: classify the two new filenames and enforce the 16 MiB aggregate cap.
  • Existing unit/integration/browser tests named below: update only contracts affected by these changes.

Task 1: Checkpoint the existing authored-soundtrack baseline

Files:

  • Commit first: the current authored intro/main integration already present in the worktree
  • Commit separately: docs/superpowers/plans/2026-09-04-portfolio-adaptive-soundtrack.md

Interfaces:

  • Consumes: the current dirty worktree containing the approved intro, main master, exact manifest, lifecycle controller, and compiled-preview fixes.

  • Produces: one named baseline commit so later adaptive commits remain reviewable without rewriting or losing the user's existing work.

  • Step 1: Confirm the worktree and preserve every existing change

Run:

git branch --show-current
git status --short
git diff --check

Expected: branch is codex/portfolio-audio-refinement; the status contains the authored-soundtrack paths recorded below; git diff --check prints nothing. Do not reset, restore, or clean this worktree.

  • Step 2: Run only the baseline-focused checks

Run:

npm run typecheck
npm run test:unit -- tests/unit/audio-lifecycle.test.ts tests/unit/soundtrack-transport.test.ts tests/unit/content-schemas.test.ts tests/integration/public-content.test.ts tests/integration/materialize-empty-asset-set.test.mjs tests/integration/verify-artifacts.test.ts tests/performance/budgets.test.ts
npm run verify:approved-assets

Expected: all three commands pass. Stop and fix only a baseline regression if one fails; do not begin adaptive work on a failing baseline.

  • Step 3: Stage the exact authored baseline

Run:

git add content/public/asset-manifest.json content/public/portfolio-content.json nginx.portfolio.conf.example
git add scripts/check-bundle-budgets.mjs scripts/lib/bundle-budgets.d.mts scripts/lib/bundle-budgets.mjs scripts/materialize-empty-asset-set.mjs scripts/smoke-production.mjs scripts/verify-artifacts.mjs
git add src/App.vue src/audio/create-audio-controller.ts src/audio/music-sequencer.ts src/audio/synth-engine.ts src/audio/types.ts src/audio/journey-music-state.ts src/audio/soundtrack-engine.ts src/audio/soundtrack-transport.ts src/components/AppShell.vue src/composables/useAudioEngine.ts src/content/schemas.ts src/content/types.ts src/security/security-policy.ts
git add tests/browser/audio-controls.spec.ts tests/deployment/seo.spec.ts tests/helpers/serve-dist.ts tests/integration/materialize-empty-asset-set.test.mjs tests/integration/nginx-config.test.ts tests/integration/public-content.test.ts tests/integration/verify-artifacts.test.ts tests/performance/budgets.test.ts tests/unit/audio-lifecycle.test.ts tests/unit/content-schemas.test.ts tests/unit/metadata.test.ts tests/unit/security-policy.test.ts tests/unit/soundtrack-transport.test.ts
git add inputs/prepared-assets/nightdrive-intro.opus inputs/prepared-assets/nightdrive-intro.mp3 inputs/prepared-assets/nightdrive-main.opus inputs/prepared-assets/nightdrive-main.mp3
git diff --cached --stat

Expected: the staged diff contains the current authored soundtrack and supporting delivery changes, including deletion of the rejected procedural sequencer/synth files. It does not contain the adaptive plan document or unrelated files.

  • Step 4: Commit the baseline

Run:

git commit -m "feat: integrate authored nightdrive soundtrack"

Expected: one commit is created. git status --short contains only the plan document.

  • Step 5: Preserve the approved execution plan as its own documentation commit

Run:

git add docs/superpowers/plans/2026-09-04-portfolio-adaptive-soundtrack.md
git commit -m "docs: plan adaptive nightdrive soundtrack"

Expected: the plan has its own commit and git status --short is empty before external music production begins.

Task 2: Produce and approve the short adaptive music additions

Files:

  • Create: /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/**
  • Read: /home/luca/Projects/nightdrive-music/generated/production/hybrid-v1/main-score-64bars-master-candidate-v4.wav
  • Read: /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/ace_client.py
  • Read: /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/build_loop.py
  • Read: /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/analyze_audio.py
  • Portfolio repository: no changes

Interfaces:

  • Consumes: approved 48 kHz main master, ACE-Step XL-SFT, 4B planner, 86 BPM/F minor/4/4, fixed seeds 44201, 44202, 44301, and 44302.

  • Produces: listener-approved nightdrive-afterglow-runtime.wav and nightdrive-drive-lift.wav, four web derivatives, exact sample/loop metadata, and production-manifest.json.

  • Step 1: Verify the local model contract and start the API only if needed

Run in the production shell:

test -L /home/luca/Projects/nightdrive-music/ACE-Step-1.5/checkpoints
test "$(readlink /home/luca/Projects/nightdrive-music/ACE-Step-1.5/checkpoints)" = "../checkpoints"
mkdir -p /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/{references,candidates,loops,verification,auditions,generation,analysis,web}
curl --silent --show-error http://127.0.0.1:8001/health

Expected: both symlink checks succeed. If health is unavailable, start the service in a separate terminal with the approved CPU offload enabled:

cd /home/luca/Projects/nightdrive-music/ACE-Step-1.5
ACESTEP_OFFLOAD_TO_CPU=true \
ACESTEP_CONFIG_PATH=acestep-v15-xl-sft \
ACESTEP_INIT_LLM=true \
ACESTEP_LM_MODEL_PATH=acestep-5Hz-lm-4B \
uv run acestep-api --host 127.0.0.1 --port 8001 \
  --init-llm --lm-model-path acestep-5Hz-lm-4B

Then run:

curl --silent --show-error http://127.0.0.1:8001/health | jq -e '.code == 200 and .data.status == "ok" and .data.models_initialized == true and .data.llm_initialized == true and .data.loaded_model == "acestep-v15-xl-sft" and .data.loaded_lm_model == "acestep-5Hz-lm-4B"'

Expected: jq exits zero and prints true. Do not replace the checkpoint symlink or download another model.

  • Step 2: Verify the approved master and cut deterministic conditioning excerpts

Run:

printf '%s  %s\n' 'f232dfd0f012c3c7aeef6013a36a3695e82cdf0de2e01aa2e8a95b2c2dd8cc3f' '/home/luca/Projects/nightdrive-music/generated/production/hybrid-v1/main-score-64bars-master-candidate-v4.wav' | sha256sum --check
ffmpeg -v error -y -i /home/luca/Projects/nightdrive-music/generated/production/hybrid-v1/main-score-64bars-master-candidate-v4.wav -af 'atrim=start_sample=7133023:end_sample=8477023,asetpts=PTS-STARTPTS' -ar 48000 -ac 2 -c:a pcm_f32le /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/references/approved-main-afterglow-reference.wav
ffmpeg -v error -y -i /home/luca/Projects/nightdrive-music/generated/production/hybrid-v1/main-score-64bars-master-candidate-v4.wav -af 'atrim=start_sample=6429767:end_sample=7501395,asetpts=PTS-STARTPTS' -ar 48000 -ac 2 -c:a pcm_f32le /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/references/approved-main-drive-lift-reference.wav

Expected: the checksum reports OK; the afterglow reference is 1,344,000 samples and ends before the approved final fade; the drive reference is 1,071,628 samples and covers the final-lift phrase.

  • Step 3: Generate two fixed-seed afterglows and two fixed-seed drive lifts

Run in a shell with pipeline failure propagation:

set -o pipefail
AFTERGLOW_PROMPT='Original instrumental adaptive afterglow companion for the supplied Nightdrive master at 86 BPM, F minor, 4/4. Write one non-repeating transition bar followed by an eight-bar seamless low-energy idle phrase and at least 250 milliseconds of musically continuous handle audio, all within a 30-second render. The transition must begin understated and naturally emerge beneath a fading master. The eight-bar region must be fatigue-resistant and circular, with warm glassy atmospheric pads, a faint pulse, restrained texture, gentle inner motion, no exposed lead, no obvious restart, and no fade to silence. Its ending and continuation must return naturally to its opening. Match the reference only for production identity, timbre, density, stereo character, and groove; compose entirely original notes and arrangement. No vocals, speech, bass lead, dramatic drums, EDM drop, riser, impact, trailer effect, harsh glitch, isolated bleeps, piercing pulse, or huge reverb.'
DRIVE_PROMPT='Original instrumental adaptive acceleration layer for the supplied Nightdrive master at 86 BPM, F minor, 4/4. Write four exact-feeling bars followed by at least 250 milliseconds of musically continuous handle audio within a 16-second render. Use restrained electronic percussion and a glassy non-dominant shimmer only: clean pulse, subtle syncopation, soft transient detail, wide but mono-safe air, and enough rhythmic identity to reward sustained acceleration while remaining clearly subordinate to background music. The ending and continuation must return naturally to the opening. Match the reference only for production identity, timbre, stereo character, and groove; compose entirely original material. No vocals, speech, bass line, chord bed, pad bed, melody, solo, fill, riser, impact, drop, trailer effect, harsh glitch, isolated bleeps, or fade to silence.'
printf '%s\n' "$AFTERGLOW_PROMPT" > /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/generation/afterglow-prompt.txt
printf '%s\n' "$DRIVE_PROMPT" > /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/generation/drive-lift-prompt.txt
for ADAPTIVE_SEED in 44201 44202; do
  /home/luca/Projects/nightdrive-music/ACE-Step-1.5/.venv/bin/python /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/ace_client.py --prompt "$AFTERGLOW_PROMPT" --output "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-${ADAPTIVE_SEED}-source.wav" --duration 30 --bpm 86 --key 'F minor' --seed "$ADAPTIVE_SEED" --reference-audio /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/references/approved-main-afterglow-reference.wav --timeout 900 | tee "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/generation/afterglow-seed-${ADAPTIVE_SEED}.json"
done
for ADAPTIVE_SEED in 44301 44302; do
  /home/luca/Projects/nightdrive-music/ACE-Step-1.5/.venv/bin/python /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/ace_client.py --prompt "$DRIVE_PROMPT" --output "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/drive-lift-seed-${ADAPTIVE_SEED}-source.wav" --duration 16 --bpm 86 --key 'F minor' --seed "$ADAPTIVE_SEED" --reference-audio /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/references/approved-main-drive-lift-reference.wav --timeout 900 | tee "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/generation/drive-lift-seed-${ADAPTIVE_SEED}.json"
done

Expected: four successful JSON task records and four WAVs. The only within-asset generation variable is the fixed seed.

  • Step 4: Screen every raw render technically before listening

Create /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh with apply_patch:

#!/usr/bin/env bash
set -euo pipefail

AUDIO_PATH="$1"
REPORT_STEM="$2"
ANALYSIS_ROOT='/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analysis'

/home/luca/Projects/nightdrive-music/ACE-Step-1.5/.venv/bin/python \
  /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/analyze_audio.py \
  "$AUDIO_PATH" | tee "${ANALYSIS_ROOT}/${REPORT_STEM}-signal.json"
ffmpeg -hide_banner -nostats -i "$AUDIO_PATH" \
  -filter_complex 'ebur128=peak=true' -f null - 2>&1 | \
  tee "${ANALYSIS_ROOT}/${REPORT_STEM}-ebur128.txt"
ffmpeg -hide_banner -nostats -i "$AUDIO_PATH" \
  -af 'silencedetect=noise=-60dB:d=0.5' -f null - 2>&1 | \
  tee "${ANALYSIS_ROOT}/${REPORT_STEM}-silence.txt"

Run it on every raw render and both references:

bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/references/approved-main-afterglow-reference.wav approved-main-afterglow-reference
bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/references/approved-main-drive-lift-reference.wav approved-main-drive-lift-reference
for ADAPTIVE_SEED in 44201 44202; do
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-${ADAPTIVE_SEED}-source.wav" "afterglow-seed-${ADAPTIVE_SEED}-source"
done
for ADAPTIVE_SEED in 44301 44302; do
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/drive-lift-seed-${ADAPTIVE_SEED}-source.wav" "drive-lift-seed-${ADAPTIVE_SEED}-source"
done

Expected: every survivor is finite 48 kHz stereo, remains below full scale and true-peak clipping, has no sustained unintended silence, is long enough for its exact loop plus handle, and is not collapsed in width or spectrum relative to its conditioning excerpt. Reject unintended vocals by listening. Do not rank composition quality from these reports.

  • Step 5: Build exact loops and exact afterglow runtimes

Create /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-afterglow-runtime.py with apply_patch. This keeps the loop builder's optimized recurring seam, then makes the last 250 ms of the one-shot transition approach the raw idle tail that naturally precedes that optimized loop opening. It therefore preserves both transition -> first idle and idle -> repeated idle instead of fixing one seam at the expense of the other:

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
from pathlib import Path

import numpy as np
import soundfile as sf

SAMPLE_RATE = 48_000
TRANSITION_SAMPLES = 133_953
LOOP_SAMPLES = 1_071_628
CROSSFADE_SAMPLES = 12_000


def seam_delta(left: np.ndarray, right: np.ndarray) -> float:
    return float(np.linalg.norm(right - left))


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument('source', type=Path)
    parser.add_argument('loop', type=Path)
    parser.add_argument('output', type=Path)
    args = parser.parse_args()

    source, source_rate = sf.read(args.source, always_2d=True, dtype='float64')
    loop, loop_rate = sf.read(args.loop, always_2d=True, dtype='float64')
    required_source_samples = TRANSITION_SAMPLES + LOOP_SAMPLES + CROSSFADE_SAMPLES
    if source_rate != SAMPLE_RATE or loop_rate != SAMPLE_RATE:
        raise ValueError('Afterglow source and loop must both be 48 kHz.')
    if source.shape[1] != 2 or loop.shape[1] != 2:
        raise ValueError('Afterglow source and loop must both be stereo.')
    if len(source) < required_source_samples or len(loop) != LOOP_SAMPLES:
        raise ValueError('Afterglow source handle or exact loop length is invalid.')

    transition = source[:TRANSITION_SAMPLES].copy()
    raw_idle_tail_start = TRANSITION_SAMPLES + LOOP_SAMPLES - CROSSFADE_SAMPLES
    raw_idle_tail = source[raw_idle_tail_start:raw_idle_tail_start + CROSSFADE_SAMPLES]
    phase = np.linspace(0.0, np.pi / 2.0, CROSSFADE_SAMPLES, endpoint=True)
    fade_out = np.square(np.cos(phase))[:, None]
    fade_in = np.square(np.sin(phase))[:, None]
    transition[-CROSSFADE_SAMPLES:] = (
        transition[-CROSSFADE_SAMPLES:] * fade_out + raw_idle_tail * fade_in
    )
    runtime = np.concatenate([transition, loop], axis=0)
    if len(runtime) != TRANSITION_SAMPLES + LOOP_SAMPLES:
        raise AssertionError('Afterglow runtime length changed during assembly.')

    args.output.parent.mkdir(parents=True, exist_ok=True)
    sf.write(args.output, runtime, SAMPLE_RATE, subtype='FLOAT')
    ordinary_deltas = np.linalg.norm(np.diff(runtime, axis=0), axis=1)
    typical = float(np.median(ordinary_deltas))
    entry = seam_delta(runtime[TRANSITION_SAMPLES - 1], runtime[TRANSITION_SAMPLES])
    recurring = seam_delta(runtime[-1], runtime[TRANSITION_SAMPLES])
    print(json.dumps({
        'source': str(args.source.resolve()),
        'loop': str(args.loop.resolve()),
        'output': str(args.output.resolve()),
        'sample_rate': SAMPLE_RATE,
        'transition_samples': TRANSITION_SAMPLES,
        'loop_samples': LOOP_SAMPLES,
        'runtime_samples': len(runtime),
        'crossfade_samples': CROSSFADE_SAMPLES,
        'transition_to_idle_seam_delta': entry,
        'transition_to_idle_vs_typical_ratio': entry / typical if typical > 0 else None,
        'idle_to_idle_seam_delta': recurring,
        'idle_to_idle_vs_typical_ratio': recurring / typical if typical > 0 else None,
    }, indent=2, sort_keys=True))


if __name__ == '__main__':
    main()

Then run:

for ADAPTIVE_SEED in 44201 44202; do
  /home/luca/Projects/nightdrive-music/ACE-Step-1.5/.venv/bin/python /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/build_loop.py "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-${ADAPTIVE_SEED}-source.wav" "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/afterglow-seed-${ADAPTIVE_SEED}-idle-loop.wav" --bpm 86 --bars 8 --beats-per-bar 4 --start-seconds 2.7906875 --crossfade-ms 250 | tee "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analysis/afterglow-seed-${ADAPTIVE_SEED}-loop-build.json"
  /home/luca/Projects/nightdrive-music/ACE-Step-1.5/.venv/bin/python /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-afterglow-runtime.py "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-${ADAPTIVE_SEED}-source.wav" "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/afterglow-seed-${ADAPTIVE_SEED}-idle-loop.wav" "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-${ADAPTIVE_SEED}-runtime.wav" | tee "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analysis/afterglow-seed-${ADAPTIVE_SEED}-runtime-build.json"
done
for ADAPTIVE_SEED in 44301 44302; do
  /home/luca/Projects/nightdrive-music/ACE-Step-1.5/.venv/bin/python /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/build_loop.py "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/drive-lift-seed-${ADAPTIVE_SEED}-source.wav" "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/drive-lift-seed-${ADAPTIVE_SEED}-loop.wav" --bpm 86 --bars 4 --beats-per-bar 4 --start-seconds 0 --crossfade-ms 250 | tee "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analysis/drive-lift-seed-${ADAPTIVE_SEED}-loop-build.json"
done

Expected: each afterglow idle is 1,071,628 samples; each runtime is 1,205,581 samples; each drive lift is 535,814 samples. Both afterglow seam ratios are finite in every runtime-build record. These ratios screen discontinuities only; both seams still require listening in Steps 7 and 8.

  • Step 6: Render three repetitions and build the two listening comparisons

Run:

for ADAPTIVE_SEED in 44201 44202; do
  LOOP_PATH="/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/afterglow-seed-${ADAPTIVE_SEED}-idle-loop.wav"
  ffmpeg -v error -y -i "$LOOP_PATH" -i "$LOOP_PATH" -i "$LOOP_PATH" -filter_complex '[0:a][1:a][2:a]concat=n=3:v=0:a=1[out]' -map '[out]' -ar 48000 -ac 2 -c:a pcm_f32le "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/afterglow-seed-${ADAPTIVE_SEED}-idle-loop-3x.wav"
  ffmpeg -v error -y -i /home/luca/Projects/nightdrive-music/generated/production/hybrid-v1/main-score-64bars-master-candidate-v4.wav -i "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-${ADAPTIVE_SEED}-runtime.wav" -i "$LOOP_PATH" -filter_complex '[0:a]atrim=start_sample=8305116:end_sample=8573023,asetpts=PTS-STARTPTS,volume=0.489778819[main];[1:a][2:a]concat=n=2:v=0:a=1,afade=t=in:ss=0:ns=133953:curve=tri,volume=0.489778819[after];[after]adelay=delays=133954S:all=1[after-delayed];[main][after-delayed]amix=inputs=2:duration=longest:normalize=0[out]' -map '[out]' -ar 48000 -ac 2 -c:a pcm_f32le "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/auditions/afterglow-seed-${ADAPTIVE_SEED}-handoff-and-seam.wav"
done
for ADAPTIVE_SEED in 44301 44302; do
  LOOP_PATH="/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/drive-lift-seed-${ADAPTIVE_SEED}-loop.wav"
  ffmpeg -v error -y -i "$LOOP_PATH" -i "$LOOP_PATH" -i "$LOOP_PATH" -filter_complex '[0:a][1:a][2:a]concat=n=3:v=0:a=1[out]' -map '[out]' -ar 48000 -ac 2 -c:a pcm_f32le "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/drive-lift-seed-${ADAPTIVE_SEED}-loop-3x.wav"
  ffmpeg -v error -y -i /home/luca/Projects/nightdrive-music/generated/production/hybrid-v1/main-score-64bars-master-candidate-v4.wav -i "$LOOP_PATH" -filter_complex '[0:a]asplit=4[m0][m1][m2][m3];[m0]atrim=start_sample=1071628:end_sample=1607442,asetpts=PTS-STARTPTS[o];[m1]atrim=start_sample=2143256:end_sample=2679070,asetpts=PTS-STARTPTS[s];[m2]atrim=start_sample=6429767:end_sample=6965581,asetpts=PTS-STARTPTS[l];[m3]atrim=start_sample=7501395:end_sample=8037209,asetpts=PTS-STARTPTS[r];[o][s][l][r]concat=n=4:v=0:a=1,volume=0.841395142[bed];[1:a]asplit=4[d0s][d1s][d2s][d3s];[d0s]afade=t=in:ss=33488:ns=33488:curve=tri,afade=t=out:ss=502326:ns=33488:curve=tri[d0];[d1s]afade=t=in:ss=33488:ns=33488:curve=tri,afade=t=out:ss=502326:ns=33488:curve=tri[d1];[d2s]afade=t=in:ss=33488:ns=33488:curve=tri,afade=t=out:ss=502326:ns=33488:curve=tri[d2];[d3s]afade=t=in:ss=33488:ns=33488:curve=tri,afade=t=out:ss=502326:ns=33488:curve=tri[d3];[d0][d1][d2][d3]concat=n=4:v=0:a=1,volume=0.125892541[lift];[bed][lift]amix=inputs=2:duration=shortest:normalize=0[out]' -map '[out]' -ar 48000 -ac 2 -c:a pcm_f32le "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/auditions/drive-lift-seed-${ADAPTIVE_SEED}-four-moods.wav"
done

Expected: afterglow three-repeat files are 3,214,884 samples; drive three-repeat files are 1,607,442 samples; afterglow auditions are 2,411,163 samples; drive auditions are 2,143,256 samples.

  • Step 7: Verify samples, analyze the built files, and listen across every seam

Run the persisted helper from Step 4 on every exact build with deterministic report names:

for ADAPTIVE_SEED in 44201 44202; do
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/afterglow-seed-${ADAPTIVE_SEED}-idle-loop.wav" "afterglow-seed-${ADAPTIVE_SEED}-idle-loop"
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-${ADAPTIVE_SEED}-runtime.wav" "afterglow-seed-${ADAPTIVE_SEED}-runtime"
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/afterglow-seed-${ADAPTIVE_SEED}-idle-loop-3x.wav" "afterglow-seed-${ADAPTIVE_SEED}-idle-loop-3x"
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/auditions/afterglow-seed-${ADAPTIVE_SEED}-handoff-and-seam.wav" "afterglow-seed-${ADAPTIVE_SEED}-audition"
done
for ADAPTIVE_SEED in 44301 44302; do
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/drive-lift-seed-${ADAPTIVE_SEED}-loop.wav" "drive-lift-seed-${ADAPTIVE_SEED}-loop"
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/drive-lift-seed-${ADAPTIVE_SEED}-loop-3x.wav" "drive-lift-seed-${ADAPTIVE_SEED}-loop-3x"
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/auditions/drive-lift-seed-${ADAPTIVE_SEED}-four-moods.wav" "drive-lift-seed-${ADAPTIVE_SEED}-audition"
done

For each built file, run:

ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate,channels,duration_ts -of json /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-44201-runtime.wav
ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate,channels,duration_ts -of json /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/drive-lift-seed-44301-loop.wav

Repeat the two probes for seeds 44202 and 44302. Expected: exact counts from Step 6, finite stereo output, no new clipping, and no unintended silence. Listen to all three-repeat files at both seams and to all four-mood drive excerpts.

  • Step 8: Stop for the musical listening checkpoint

Present only the strongest afterglow audition and strongest drive audition, with one runner-up only if the choice is genuinely ambiguous. Use playable local audio, for example:

![Afterglow — seed 44201](/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/auditions/afterglow-seed-44201-handoff-and-seam.wav)
![Drive lift — seed 44301](/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/auditions/drive-lift-seed-44301-four-moods.wav)

State the exact seed and reference path for each file, and state that measurements establish technical fitness rather than musical quality. Ask for explicit approval of both additions. Do not edit or copy anything into the Portfolio repository before approval. If neither drive candidate remains unobtrusive across all four excerpts, stop and ask whether to omit drive lift or continue production.

  • Step 9: Canonicalize only the approved pair and encode the web derivatives

After explicit approval, run exactly one command from each pair:

cp /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-44201-runtime.wav /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-afterglow-runtime.wav
cp /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/candidates/afterglow-seed-44202-runtime.wav /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-afterglow-runtime.wav
cp /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/drive-lift-seed-44301-loop.wav /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-drive-lift.wav
cp /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/loops/drive-lift-seed-44302-loop.wav /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-drive-lift.wav

Then run:

ffmpeg -v error -y -i /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-afterglow-runtime.wav -map 0:a:0 -ar 48000 -ac 2 -c:a libopus -b:a 160k -vbr on -compression_level 10 /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.opus
ffmpeg -v error -y -i /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-afterglow-runtime.wav -map 0:a:0 -ar 48000 -ac 2 -c:a libmp3lame -b:a 192k -write_xing 1 /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.mp3
ffmpeg -v error -y -i /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-drive-lift.wav -map 0:a:0 -ar 48000 -ac 2 -c:a libopus -b:a 160k -vbr on -compression_level 10 /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.opus
ffmpeg -v error -y -i /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-drive-lift.wav -map 0:a:0 -ar 48000 -ac 2 -c:a libmp3lame -b:a 192k -write_xing 1 /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.mp3

Expected: four short derivatives are created without modifying the existing intro/main encodes.

  • Step 10: Decode, verify, checksum, and record the approved delivery

Run:

for CODEC in opus mp3; do
  ffmpeg -v error -y -i "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.${CODEC}" -ar 48000 -ac 2 -c:a pcm_f32le "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/nightdrive-afterglow-${CODEC}-decoded.wav"
  ffmpeg -v error -y -i "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.${CODEC}" -ar 48000 -ac 2 -c:a pcm_f32le "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/nightdrive-drive-lift-${CODEC}-decoded.wav"
done
for DECODED_PATH in /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/nightdrive-*-decoded.wav; do
  ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate,channels,duration_ts -of json "$DECODED_PATH"
done
sha256sum /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-afterglow-runtime.wav /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-drive-lift.wav /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.opus /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.mp3 /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.opus /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.mp3 | tee /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analysis/delivery-sha256.txt
stat -c '%n %s' /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.opus /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.mp3 /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.opus /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.mp3 | tee /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analysis/web-bytes.txt
stat -c '%s' /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.opus /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.mp3 /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.opus /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.mp3 | awk '{new_bytes += $1} END {total_bytes = 13603356 + new_bytes; printf "deferred audio: %d / 16777216 bytes\n", total_bytes; exit(total_bytes > 16777216)}'

Expected: decoded afterglow files report 1,205,581 samples; decoded drive files report 535,814 samples; the byte precheck exits zero below 16,777,216 bytes. Analyze all four decoded WAVs with the persisted Step 4 helper and compare them with their masters; do not normalize or remaster them.

Run the analyzer on every decoded derivative with fixed report names:

for CODEC in opus mp3; do
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/nightdrive-afterglow-${CODEC}-decoded.wav" "nightdrive-afterglow-${CODEC}-decoded"
  bash /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/analyze-one.sh "/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/nightdrive-drive-lift-${CODEC}-decoded.wav" "nightdrive-drive-lift-${CODEC}-decoded"
done

Expected: all four analyzer records are finite stereo, the lossy encodes introduce no sustained silence or clipping, and their decoded durations remain exact.

  • Step 11: Assemble the production manifest deterministically

Create /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-production-manifest.mjs with apply_patch:

import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { readFile, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';

const ROOT = '/home/luca/Projects/nightdrive-music/generated/production/adaptive-v1';
const APPROVED_MAIN = '/home/luca/Projects/nightdrive-music/generated/production/hybrid-v1/main-score-64bars-master-candidate-v4.wav';
const EXISTING_AUDIO_BYTES = 13_603_356;
const DEFERRED_AUDIO_CAP_BYTES = 16_777_216;
const AFTERGLOW_SEEDS = new Set([44201, 44202]);
const DRIVE_SEEDS = new Set([44301, 44302]);
const approvedAfterglowSeed = Number(process.env.ADAPTIVE_AFTERGLOW_SEED);
const approvedDriveLiftSeed = Number(process.env.ADAPTIVE_DRIVE_SEED);

if (!AFTERGLOW_SEEDS.has(approvedAfterglowSeed)) {
  throw new Error('ADAPTIVE_AFTERGLOW_SEED must be 44201 or 44202');
}
if (!DRIVE_SEEDS.has(approvedDriveLiftSeed)) {
  throw new Error('ADAPTIVE_DRIVE_SEED must be 44301 or 44302');
}

const pathAt = (...parts) => join(ROOT, ...parts);
const readText = (path) => readFile(path, 'utf8');
const readJson = async (path) => JSON.parse(await readText(path));

async function sha256(path) {
  const hash = createHash('sha256');
  hash.update(await readFile(path));
  return hash.digest('hex');
}

async function fileRecord(path) {
  const metadata = await stat(path);
  return { path, bytes: metadata.size, sha256: await sha256(path) };
}

function probe(path) {
  const output = execFileSync('ffprobe', [
    '-v', 'error',
    '-select_streams', 'a:0',
    '-show_entries', 'stream=sample_rate,channels,duration_ts,codec_name',
    '-of', 'json',
    path,
  ], { encoding: 'utf8' });
  const stream = JSON.parse(output).streams?.[0];
  if (!stream) throw new Error(`No audio stream in ${path}`);
  return {
    codec: stream.codec_name,
    sampleRate: Number(stream.sample_rate),
    channels: Number(stream.channels),
    durationSamples: Number(stream.duration_ts),
  };
}

function expectAudio(path, expectedSamples) {
  const result = probe(path);
  if (result.sampleRate !== 48_000 || result.channels !== 2 || result.durationSamples !== expectedSamples) {
    throw new Error(`Unexpected audio contract for ${path}: ${JSON.stringify(result)}`);
  }
  return result;
}

function parseEbur128(text, reportPath) {
  const summary = text.slice(text.lastIndexOf('Summary:'));
  const number = (pattern, label) => {
    const match = summary.match(pattern);
    if (!match) throw new Error(`Missing ${label} in ${reportPath}`);
    return Number(match[1]);
  };
  return {
    integratedLufs: number(/I:\s+(-?\d+(?:\.\d+)?)\s+LUFS/, 'integrated loudness'),
    loudnessRangeLu: number(/LRA:\s+(\d+(?:\.\d+)?)\s+LU/, 'loudness range'),
    truePeakDbfs: number(/Peak:\s+(-?\d+(?:\.\d+)?)\s+dBFS/, 'true peak'),
  };
}

async function technicalScreen(stem) {
  const signalPath = pathAt('analysis', `${stem}-signal.json`);
  const eburPath = pathAt('analysis', `${stem}-ebur128.txt`);
  const silencePath = pathAt('analysis', `${stem}-silence.txt`);
  const silenceText = await readText(silencePath);
  return {
    analyzer: await readJson(signalPath),
    ffmpegEbur128: parseEbur128(await readText(eburPath), eburPath),
    ffmpegSilenceEvents: silenceText.split('\n').filter((line) => line.includes('silence_')),
    reportFiles: await Promise.all([signalPath, eburPath, silencePath].map(fileRecord)),
  };
}

async function candidate(kind, seed, approved) {
  const prefix = kind === 'afterglow' ? 'afterglow' : 'drive-lift';
  const sourceStem = `${prefix}-seed-${seed}-source`;
  const sourcePath = pathAt('candidates', `${sourceStem}.wav`);
  const generationPath = pathAt('generation', `${prefix}-seed-${seed}.json`);
  const promptPath = pathAt('generation', `${kind === 'afterglow' ? 'afterglow' : 'drive-lift'}-prompt.txt`);
  return {
    kind,
    seed,
    referencePath: pathAt('references', kind === 'afterglow'
      ? 'approved-main-afterglow-reference.wav'
      : 'approved-main-drive-lift-reference.wav'),
    prompt: (await readText(promptPath)).trim(),
    generation: await readJson(generationPath),
    source: await fileRecord(sourcePath),
    technicalScreen: await technicalScreen(sourceStem),
    disposition: approved ? 'listener-approved' : 'technically-valid-not-selected-by-listener',
  };
}

async function derivative(kind, codec, expectedSamples) {
  const base = kind === 'afterglow' ? 'nightdrive-afterglow' : 'nightdrive-drive-lift';
  const encodedPath = pathAt('web', `${base}.${codec}`);
  const decodedStem = `${base}-${codec}-decoded`;
  const decodedPath = pathAt('verification', `${decodedStem}.wav`);
  return {
    kind,
    codec,
    encoded: await fileRecord(encodedPath),
    decoded: await fileRecord(decodedPath),
    decodedProbe: expectAudio(decodedPath, expectedSamples),
    decodedTechnicalScreen: await technicalScreen(decodedStem),
  };
}

const afterglowReference = pathAt('references', 'approved-main-afterglow-reference.wav');
const driveReference = pathAt('references', 'approved-main-drive-lift-reference.wav');
const afterglowRuntime = pathAt('nightdrive-afterglow-runtime.wav');
const driveLiftMaster = pathAt('nightdrive-drive-lift.wav');
const afterglowLoop = pathAt('loops', `afterglow-seed-${approvedAfterglowSeed}-idle-loop.wav`);
const driveLoop = pathAt('loops', `drive-lift-seed-${approvedDriveLiftSeed}-loop.wav`);
const afterglowThree = pathAt('verification', `afterglow-seed-${approvedAfterglowSeed}-idle-loop-3x.wav`);
const driveThree = pathAt('verification', `drive-lift-seed-${approvedDriveLiftSeed}-loop-3x.wav`);
const afterglowAudition = pathAt('auditions', `afterglow-seed-${approvedAfterglowSeed}-handoff-and-seam.wav`);
const driveAudition = pathAt('auditions', `drive-lift-seed-${approvedDriveLiftSeed}-four-moods.wav`);

expectAudio(afterglowReference, 1_344_000);
expectAudio(driveReference, 1_071_628);
expectAudio(afterglowRuntime, 1_205_581);
expectAudio(driveLiftMaster, 535_814);
expectAudio(afterglowLoop, 1_071_628);
expectAudio(driveLoop, 535_814);
expectAudio(afterglowThree, 3_214_884);
expectAudio(driveThree, 1_607_442);

const webDerivatives = await Promise.all([
  derivative('afterglow', 'opus', 1_205_581),
  derivative('afterglow', 'mp3', 1_205_581),
  derivative('driveLift', 'opus', 535_814),
  derivative('driveLift', 'mp3', 535_814),
]);
const newAudioBytes = webDerivatives.reduce((sum, item) => sum + item.encoded.bytes, 0);
const deferredAudioBytes = EXISTING_AUDIO_BYTES + newAudioBytes;
if (deferredAudioBytes > DEFERRED_AUDIO_CAP_BYTES) {
  throw new Error(`Deferred audio ${deferredAudioBytes} exceeds ${DEFERRED_AUDIO_CAP_BYTES}`);
}

const manifest = {
  schema: 'nightdrive-adaptive-production/v1',
  musicalContract: { bpm: 86, key: 'F minor', timeSignature: '4/4', sampleRate: 48_000 },
  models: { generator: 'acestep-v15-xl-sft', planner: 'acestep-5Hz-lm-4B', cpuOffload: true },
  references: [
    {
      role: 'afterglow-conditioning',
      sourceMaster: await fileRecord(APPROVED_MAIN),
      sourceSampleRange: [7_133_023, 8_477_023],
      excerpt: await fileRecord(afterglowReference),
      technicalScreen: await technicalScreen('approved-main-afterglow-reference'),
    },
    {
      role: 'drive-lift-conditioning',
      sourceMaster: await fileRecord(APPROVED_MAIN),
      sourceSampleRange: [6_429_767, 7_501_395],
      excerpt: await fileRecord(driveReference),
      technicalScreen: await technicalScreen('approved-main-drive-lift-reference'),
    },
  ],
  candidates: await Promise.all([
    candidate('afterglow', 44201, approvedAfterglowSeed === 44201),
    candidate('afterglow', 44202, approvedAfterglowSeed === 44202),
    candidate('driveLift', 44301, approvedDriveLiftSeed === 44301),
    candidate('driveLift', 44302, approvedDriveLiftSeed === 44302),
  ]),
  selection: {
    approvedByListener: true,
    approvedAfterglowSeed,
    approvedDriveLiftSeed,
    checkpoint: 'Explicit human approval after in-context audition; measurements used only for technical screening.',
  },
  afterglow: {
    transitionSamples: 133_953,
    loopStartSample: 133_953,
    loopEndSample: 1_205_581,
    loopBars: 8,
    crossfadeSamples: 12_000,
    runtime: await fileRecord(afterglowRuntime),
    runtimeProbe: expectAudio(afterglowRuntime, 1_205_581),
    runtimeTechnicalScreen: await technicalScreen(`afterglow-seed-${approvedAfterglowSeed}-runtime`),
    loop: await fileRecord(afterglowLoop),
    loopBuild: await readJson(pathAt('analysis', `afterglow-seed-${approvedAfterglowSeed}-loop-build.json`)),
    runtimeBuild: await readJson(pathAt('analysis', `afterglow-seed-${approvedAfterglowSeed}-runtime-build.json`)),
    threeRepeatSamples: 3_214_884,
    threeRepeat: await fileRecord(afterglowThree),
    threeRepeatTechnicalScreen: await technicalScreen(`afterglow-seed-${approvedAfterglowSeed}-idle-loop-3x`),
    audition: await fileRecord(afterglowAudition),
  },
  driveLift: {
    loopStartSample: 0,
    loopEndSample: 535_814,
    loopBars: 4,
    crossfadeSamples: 12_000,
    master: await fileRecord(driveLiftMaster),
    masterProbe: expectAudio(driveLiftMaster, 535_814),
    masterTechnicalScreen: await technicalScreen(`drive-lift-seed-${approvedDriveLiftSeed}-loop`),
    loop: await fileRecord(driveLoop),
    loopBuild: await readJson(pathAt('analysis', `drive-lift-seed-${approvedDriveLiftSeed}-loop-build.json`)),
    threeRepeatSamples: 1_607_442,
    threeRepeat: await fileRecord(driveThree),
    threeRepeatTechnicalScreen: await technicalScreen(`drive-lift-seed-${approvedDriveLiftSeed}-loop-3x`),
    audition: await fileRecord(driveAudition),
  },
  masters: [
    { kind: 'afterglow', file: await fileRecord(afterglowRuntime) },
    { kind: 'driveLift', file: await fileRecord(driveLiftMaster) },
  ],
  webDerivatives,
  deliveryBudget: {
    existingAudioBytes: EXISTING_AUDIO_BYTES,
    newAudioBytes,
    deferredAudioBytes,
    capBytes: DEFERRED_AUDIO_CAP_BYTES,
  },
  qualityStatement: 'Measurements establish technical fitness only; musical approval was a human listening decision.',
};

await writeFile(pathAt('production-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);

Run exactly one command matching the listener-approved pair:

ADAPTIVE_AFTERGLOW_SEED=44201 ADAPTIVE_DRIVE_SEED=44301 node /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-production-manifest.mjs
ADAPTIVE_AFTERGLOW_SEED=44201 ADAPTIVE_DRIVE_SEED=44302 node /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-production-manifest.mjs
ADAPTIVE_AFTERGLOW_SEED=44202 ADAPTIVE_DRIVE_SEED=44301 node /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-production-manifest.mjs
ADAPTIVE_AFTERGLOW_SEED=44202 ADAPTIVE_DRIVE_SEED=44302 node /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/build-production-manifest.mjs

Then validate the assembled record:

jq -e '
  .schema == "nightdrive-adaptive-production/v1" and
  .selection.approvedByListener == true and
  (.candidates | length) == 4 and
  ([.candidates[] | select(.disposition == "listener-approved")] | length) == 2 and
  .afterglow.runtimeProbe.durationSamples == 1205581 and
  .driveLift.masterProbe.durationSamples == 535814 and
  .deliveryBudget.deferredAudioBytes <= .deliveryBudget.capBytes
' /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/production-manifest.json

Expected: the builder exits zero, writes a fully populated manifest from saved evidence, and jq prints true. The manifest records all four task IDs through their generation records, both exact prompts, reference source ranges and hashes, candidate dispositions, analyzer/FFmpeg reports, loop seam metrics, three-repeat hashes, canonical master hashes, derivative bytes/hashes/decoded sample counts, and explicit listener approval. Do not begin Task 3 until this gate passes.

Task 3: Publish the approved adaptive asset contract

Files:

  • Modify: src/content/types.ts:136-145
  • Modify: src/content/schemas.ts:62-65,202-206,313-328
  • Modify: content/public/portfolio-content.json:248-257
  • Modify: content/public/asset-manifest.json:3-50
  • Modify: scripts/materialize-empty-asset-set.mjs:131-146
  • Modify: scripts/check-bundle-budgets.mjs:70-82
  • Modify: scripts/lib/bundle-budgets.mjs:1-50
  • Modify: scripts/lib/bundle-budgets.d.mts:1
  • Create: inputs/prepared-assets/nightdrive-afterglow.opus
  • Create: inputs/prepared-assets/nightdrive-afterglow.mp3
  • Create: inputs/prepared-assets/nightdrive-drive-lift.opus
  • Create: inputs/prepared-assets/nightdrive-drive-lift.mp3
  • Modify: tests/unit/content-schemas.test.ts:17-75
  • Modify: tests/integration/public-content.test.ts:39-52
  • Modify: tests/integration/materialize-empty-asset-set.test.mjs
  • Modify: tests/integration/verify-artifacts.test.ts
  • Modify: tests/performance/budgets.test.ts:31-67,121-136

Interfaces:

  • Consumes: the explicit listener approval and exact hashes/durations in /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/production-manifest.json.

  • Produces: strict SoundtrackContent { intro, main, afterglow, driveLift }, eight exact manifest references, four prepared adaptive derivatives, and a 16 MiB aggregate deferred-audio gate.

  • Step 1: Write the failing content and budget assertions

Extend the mutable fixture soundtrack type and closure fixture in tests/unit/content-schemas.test.ts:

soundtrack?: {
  intro: { opusAssetId: string; mp3AssetId: string };
  main: { opusAssetId: string; mp3AssetId: string };
  afterglow: { opusAssetId: string; mp3AssetId: string };
  driveLift: { opusAssetId: string; mp3AssetId: string };
};

Use this exact soundtrack object in the acceptance case:

content.soundtrack = {
  intro: { opusAssetId: 'soundtrack-intro-opus', mp3AssetId: 'soundtrack-intro-mp3' },
  main: { opusAssetId: 'soundtrack-main-opus', mp3AssetId: 'soundtrack-main-mp3' },
  afterglow: { opusAssetId: 'soundtrack-afterglow-opus', mp3AssetId: 'soundtrack-afterglow-mp3' },
  driveLift: { opusAssetId: 'soundtrack-drive-lift-opus', mp3AssetId: 'soundtrack-drive-lift-mp3' },
};

Add four fixture asset records with width: null, height: null, positive durationMs, and the exact IDs/filenames above. Extend publishedReferences in tests/integration/public-content.test.ts with the four new IDs. Add these assertions to tests/performance/budgets.test.ts:

expect(PERFORMANCE_BUDGETS.deferredAudio).toBe(16 * 1024 * 1024);
expect(classifyArtifact('assets/nightdrive-afterglow.opus', '')).toBe('soundtrack-audio');
expect(classifyArtifact('assets/nightdrive-afterglow.mp3', '')).toBe('soundtrack-audio');
expect(classifyArtifact('assets/nightdrive-drive-lift.opus', '')).toBe('soundtrack-audio');
expect(classifyArtifact('assets/nightdrive-drive-lift.mp3', '')).toBe('soundtrack-audio');

In the materialization and artifact fixtures, add all four new files and assert that omitting, altering, or adding an unreferenced adaptive file still fails closed.

  • Step 2: Run the focused contract tests and verify the new assertions fail

Run:

npm run test:unit -- tests/unit/content-schemas.test.ts tests/integration/public-content.test.ts tests/integration/materialize-empty-asset-set.test.mjs tests/integration/verify-artifacts.test.ts tests/performance/budgets.test.ts

Expected: FAIL because afterglow and driveLift are not accepted or referenced, the filename classifier rejects them, and the budget is still 13 MiB.

  • Step 3: Extend the strict typed content boundary

Replace SoundtrackContent in src/content/types.ts with:

export interface SoundtrackContent {
  intro: SoundtrackAssetSources;
  main: SoundtrackAssetSources;
  afterglow: SoundtrackAssetSources;
  driveLift: SoundtrackAssetSources;
}

Use the same four required fields in the strict Zod object:

soundtrack: z.object({
  intro: soundtrackAssetSourcesSchema,
  main: soundtrackAssetSourcesSchema,
  afterglow: soundtrackAssetSourcesSchema,
  driveLift: soundtrackAssetSourcesSchema,
}).strict().optional(),

Extend parseProductionInputSnapshot with:

content.soundtrack.afterglow.opusAssetId,
content.soundtrack.afterglow.mp3AssetId,
content.soundtrack.driveLift.opusAssetId,
content.soundtrack.driveLift.mp3AssetId,

In scripts/materialize-empty-asset-set.mjs, replace the two-region loop with:

for (const regionName of ['intro', 'main', 'afterglow', 'driveLift']) {
  const region = content.soundtrack[regionName];
  if (typeof region !== 'object' || region === null || Array.isArray(region)) {
    throw new Error(`Portfolio soundtrack ${regionName} must be an object.`);
  }
  references.push(region.opusAssetId, region.mp3AssetId);
}

In scripts/check-bundle-budgets.mjs, classify the source pairs through one complete region list:

for (const region of [
  content.soundtrack.intro,
  content.soundtrack.main,
  content.soundtrack.afterglow,
  content.soundtrack.driveLift,
]) {
  if (region?.opusAssetId) assetClassById.set(region.opusAssetId, 'soundtrack-audio');
  if (region?.mp3AssetId) assetClassById.set(region.mp3AssetId, 'soundtrack-audio');
}
  • Step 4: Raise only the approved audio cap and filename classifier

Use these exact values in scripts/lib/bundle-budgets.mjs:

deferredAudio: 16 * 1024 * 1024,
if (/^assets\/nightdrive-(?:intro|main|afterglow|drive-lift)\.(?:opus|mp3)$/u.test(path)) {
  return 'soundtrack-audio';
}

Keep every other budget and classifier unchanged. The declaration file continues exporting the same public Readonly<Record<string, number>> contract.

  • Step 5: Copy the approved binaries and add exact public records

Run:

cp /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.opus inputs/prepared-assets/nightdrive-afterglow.opus
cp /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-afterglow.mp3 inputs/prepared-assets/nightdrive-afterglow.mp3
cp /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.opus inputs/prepared-assets/nightdrive-drive-lift.opus
cp /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/web/nightdrive-drive-lift.mp3 inputs/prepared-assets/nightdrive-drive-lift.mp3
sha256sum inputs/prepared-assets/nightdrive-afterglow.opus inputs/prepared-assets/nightdrive-afterglow.mp3 inputs/prepared-assets/nightdrive-drive-lift.opus inputs/prepared-assets/nightdrive-drive-lift.mp3
for AUDIO_PATH in inputs/prepared-assets/nightdrive-afterglow.opus inputs/prepared-assets/nightdrive-afterglow.mp3 inputs/prepared-assets/nightdrive-drive-lift.opus inputs/prepared-assets/nightdrive-drive-lift.mp3; do
  ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$AUDIO_PATH"
done

Add this exact content shape to content/public/portfolio-content.json:

"afterglow": {
  "opusAssetId": "soundtrack-afterglow-opus",
  "mp3AssetId": "soundtrack-afterglow-mp3"
},
"driveLift": {
  "opusAssetId": "soundtrack-drive-lift-opus",
  "mp3AssetId": "soundtrack-drive-lift-mp3"
}

Add four records to content/public/asset-manifest.json using IDs and filenames from Step 1, the SHA-256 values printed above, rounded verified durations from FFprobe, width: null, height: null, and the same rights holder, license, and attribution fields as intro/main. Compare those values against the external production manifest before saving; do not transcribe an inferred digest or duration.

  • Step 6: Run the focused contract and exact-asset checks

Run:

npm run test:unit -- tests/unit/content-schemas.test.ts tests/integration/public-content.test.ts tests/integration/materialize-empty-asset-set.test.mjs tests/integration/verify-artifacts.test.ts tests/performance/budgets.test.ts
npm run verify:approved-assets

Expected: tests pass; all eight soundtrack references form an exact manifest closure; all file hashes match. The aggregate deferred-audio total remains below 16,777,216 bytes in the external byte precheck and is checked against the compiled dist/assets in Task 10.

  • Step 7: Commit the adaptive asset contract

Run:

git add src/content/types.ts src/content/schemas.ts content/public/portfolio-content.json content/public/asset-manifest.json
git add scripts/materialize-empty-asset-set.mjs scripts/check-bundle-budgets.mjs scripts/lib/bundle-budgets.mjs scripts/lib/bundle-budgets.d.mts
git add inputs/prepared-assets/nightdrive-afterglow.opus inputs/prepared-assets/nightdrive-afterglow.mp3 inputs/prepared-assets/nightdrive-drive-lift.opus inputs/prepared-assets/nightdrive-drive-lift.mp3
git add tests/unit/content-schemas.test.ts tests/integration/public-content.test.ts tests/integration/materialize-empty-asset-set.test.mjs tests/integration/verify-artifacts.test.ts tests/performance/budgets.test.ts
git commit -m "feat: add adaptive soundtrack layer assets"

Expected: the commit contains only the approved additions and their delivery contract.

Task 4: Expose stable navigation and section geometry signals

Files:

  • Modify: src/journey/navigation-controller.ts:8-13,122-224,278-352
  • Modify: src/journey/types.ts:69-74
  • Modify: src/journey/section-controller.ts:1-65
  • Modify: src/composables/useJourneySections.ts:19-176
  • Modify: src/components/AppShell.vue:62-67
  • Modify: tests/unit/navigation-controller.test.ts:22-311
  • Modify: tests/unit/section-controller.test.ts

Interfaces:

  • Consumes: canonical section IDs, existing navigation epochs, the existing single useScrollDrive frame clock, cached heading document positions, and responsive ResizeObserver invalidation.
  • Produces: NavigationState.phase, SectionActivationSnapshot, and JourneySectionsBinding, which Task 5 consumes without adding a second scroll listener or DOM measurement pass.

Define these exact public contracts:

export type NavigationPhase = 'idle' | 'travelling' | 'settled';

export interface NavigationState {
  readonly epoch: number;
  readonly source: JourneySource;
  readonly phase: NavigationPhase;
  readonly destinationId: SectionId | null;
  readonly signDestinationId: SectionId | null;
}
export interface SectionActivationSnapshot {
  readonly layoutRevision: number;
  readonly activeHeadingOffsetVh: number;
  readonly nextHeadingOffsetVh: number | null;
}

export interface JourneySnapshot extends ScrollDriveSnapshot {
  readonly activeSectionId: SectionId;
  readonly chapter: EnvironmentChapter;
  readonly sign: SignSnapshot | null;
  readonly arrived: boolean;
  readonly sectionActivation: SectionActivationSnapshot;
}
export interface JourneySectionsBinding {
  readonly snapshot: Readonly<Ref<JourneySnapshot>>;
  requestFrame(): void;
  subscribeFrame(
    listener: (now: number, snapshot: JourneySnapshot) => boolean,
  ): () => void;
}
  • Step 1: Write failing navigation-phase tests

Update existing expected states with phase. Add a latest-epoch settlement test:

it('publishes travelling and settled only for the latest explicit epoch', () => {
  const harness = createRuntime(true);
  const controller = createNavigationController(sectionIds, harness.runtime);
  const states: NavigationState[] = [];
  controller.subscribe((state) => states.push(state));

  controller.navigate('contact', target('contact'));
  controller.navigate('toolkit', target('toolkit'));
  expect(controller.state()).toMatchObject({
    epoch: 2,
    source: 'explicit-navigation',
    phase: 'travelling',
    destinationId: 'toolkit',
  });

  harness.emitScrollEnd();
  expect(controller.state()).toMatchObject({
    epoch: 2,
    phase: 'settled',
    destinationId: 'toolkit',
  });
  expect(states.filter((state) => state.phase === 'settled')).toHaveLength(1);
});

Also assert that noteUserScrollIntent() produces phase: 'idle', initialize() settles after its instant alignment, and restore() remains travelling until its guarded frame performs instant alignment and then emits settled.

  • Step 2: Write failing activation-geometry tests

Add this case to tests/unit/section-controller.test.ts:

it('derives responsive heading offsets from the same activation-line geometry', () => {
  expect(deriveSectionActivationSnapshot({
    headings: [
      { id: 'start', top: 100 },
      { id: 'profile', top: 420 },
      { id: 'experience', top: 1_120 },
    ],
    activeSectionId: 'profile',
    activationY: 500,
    viewportHeight: 800,
    layoutRevision: 7,
  })).toEqual({
    layoutRevision: 7,
    activeHeadingOffsetVh: -0.1,
    nextHeadingOffsetVh: 0.775,
  });
});

Add a second assertion for the final heading returning nextHeadingOffsetVh: null.

  • Step 3: Run the focused journey tests and verify they fail

Run:

npm run test:unit -- tests/unit/navigation-controller.test.ts tests/unit/section-controller.test.ts tests/unit/scroll-drive-controller.test.ts

Expected: FAIL because phase and normalized activation geometry do not exist.

  • Step 4: Publish guarded navigation settlement

Initialize navigation with an idle phase:

let currentState: NavigationState = {
  epoch: 0,
  source: 'initial',
  phase: 'idle',
  destinationId: null,
  signDestinationId: null,
};

Have beginEpoch set phase solely from source:

currentState = {
  epoch: currentState.epoch + 1,
  source,
  phase: source === 'passive-scroll' ? 'idle' : 'travelling',
  destinationId,
  signDestinationId,
};

Inside the existing epoch/source/destination guards, settle before scheduling focus:

currentState = { ...currentState, phase: 'settled' };
emit();
frameId = runtime.requestFrame(() => {
  frameId = undefined;
  if (
    currentState.epoch === epoch &&
    currentState.source === 'explicit-navigation' &&
    currentState.destinationId === id
  ) {
    runtime.focusHeading(target);
  }
});

For initial, perform the instant scroll, set the same guarded epoch to settled, and emit synchronously. For history-restoration, keep travelling until its existing frame performs the instant scroll; then set settled and emit without focusing. An older frame or scrollend must never settle a newer epoch.

  • Step 5: Derive activation offsets as pure geometry

Add to src/journey/section-controller.ts:

export interface SectionActivationInput {
  readonly headings: readonly SectionHeadingGeometry[];
  readonly activeSectionId: SectionId;
  readonly activationY: number;
  readonly viewportHeight: number;
  readonly layoutRevision: number;
}

export function deriveSectionActivationSnapshot(
  input: SectionActivationInput,
): SectionActivationSnapshot {
  const activeIndex = input.headings.findIndex(
    (heading) => heading.id === input.activeSectionId,
  );
  const activeHeading = input.headings[activeIndex];
  if (activeHeading === undefined || input.viewportHeight <= 0) {
    return {
      layoutRevision: input.layoutRevision,
      activeHeadingOffsetVh: 0,
      nextHeadingOffsetVh: null,
    };
  }
  const nextHeading = input.headings[activeIndex + 1];
  return {
    layoutRevision: input.layoutRevision,
    activeHeadingOffsetVh:
      (activeHeading.top - input.activationY) / input.viewportHeight,
    nextHeadingOffsetVh: nextHeading === undefined
      ? null
      : (nextHeading.top - input.activationY) / input.viewportHeight,
  };
}

Import SectionActivationSnapshot from src/journey/types.ts.

  • Step 6: Expose the post-measurement shared-frame binding

In useJourneySections, add sectionActivation to the initial snapshot, a monotonically increasing layoutRevision, and a local subscriber set:

const frameSubscribers = new Set<(
  now: number,
  snapshot: JourneySnapshot,
) => boolean>();

let layoutRevision = 0;
if (structureDirty) {
  headings = readHeadings(driveSnapshot.scrollY);
  layoutRevision += 1;
  structureDirty = false;
}

Derive and publish geometry in the same measurement that selects the visual section:

const sectionActivation = deriveSectionActivationSnapshot({
  headings,
  activeSectionId,
  activationY,
  viewportHeight,
  layoutRevision,
});
sectionSnapshot.value = {
  ...driveSnapshot,
  routeProgress: atDocumentEnd ? 1 : driveSnapshot.routeProgress,
  ...sectionState,
  sectionActivation,
  sign,
};

let keepRunning = signController.needsFrame();
for (const subscriber of [...frameSubscribers]) {
  keepRunning = subscriber(now, snapshot.value) || keepRunning;
}
return keepRunning;

Return the complete binding and clear subscribers at teardown:

return {
  snapshot: readonly(snapshot),
  requestFrame: () => requestJourneyFrame(),
  subscribeFrame(listener) {
    frameSubscribers.add(listener);
    return () => frameSubscribers.delete(listener);
  },
};

Do not add a scroll listener or another call to getBoundingClientRect().

  • Step 7: Adapt the current caller without changing audio behavior yet

In AppShell.vue, retain a ref named journey while exposing the binding for Task 5:

const journeyBinding = useJourneySections(
  canonicalSectionIds,
  navigation.state,
  computed(() => motion.policy.value.allowSceneRequest),
);
const journey = journeyBinding.snapshot;

All existing journey.value consumers stay unchanged in this task.

  • Step 8: Run focused tests and typecheck

Run:

npm run test:unit -- tests/unit/navigation-controller.test.ts tests/unit/section-controller.test.ts tests/unit/scroll-drive-controller.test.ts
npm run typecheck

Expected: all checks pass, one raw scroll listener remains covered by scroll-drive-controller.test.ts, and responsive geometry is carried in the shared snapshot.

  • Step 9: Commit the stable journey signals

Run:

git add src/journey/navigation-controller.ts src/journey/types.ts src/journey/section-controller.ts src/composables/useJourneySections.ts src/components/AppShell.vue tests/unit/navigation-controller.test.ts tests/unit/section-controller.test.ts
git commit -m "feat: expose stable journey intent signals"

Expected: one independently passing journey-signal commit.

Task 5: Implement the pure audio-intent coordinator

Files:

  • Modify: src/audio/types.ts:9-23
  • Modify: src/audio/journey-music-state.ts
  • Create: src/audio/audio-intent-coordinator.ts
  • Create: src/composables/useAudioIntent.ts
  • Create: tests/unit/audio-intent-coordinator.test.ts
  • Create: tests/unit/use-audio-intent.test.ts

Interfaces:

  • Consumes: JourneySnapshot.sectionActivation, NavigationState.phase, canonical section order, performance.now() timestamps, visibility, and existing normalized scroll acceleration.
  • Produces: stable ChapterIntent, rate-limited DriveEnergySample, and needsFrame; Task 6 sends these through independent controller commands.

Add these shared types without removing AudioJourneyState until Task 6:

export type MusicMood = 'open' | 'shadow' | 'lift' | 'resolve';
export type MusicChapterBias = MusicMood;
export type DriveEnergySource = 'physical' | 'navigation' | 'idle';

export interface ChapterIntent {
  readonly sectionId: SectionId;
  readonly mood: MusicMood;
  readonly atStart: boolean;
  readonly entryPhrase: number;
  readonly generation: number;
}

export interface DriveEnergySample {
  readonly energy: number;
  readonly source: DriveEnergySource;
}

export interface AudioInitialState {
  readonly chapter: ChapterIntent;
  readonly drive: DriveEnergySample;
}
  • Step 1: Write the coordinator's high-value failing tests

Create tests/unit/audio-intent-coordinator.test.ts with a complete default sample helper:

const sectionIds = [
  'start', 'profile', 'experience', 'infrastructure',
  'selected-builds', 'additional-work', 'toolkit', 'contact',
] as const satisfies readonly SectionId[];

const baseSample = (
  overrides: Partial<AudioIntentSample> = {},
): AudioIntentSample => ({
  now: 0,
  visible: true,
  scrollY: 0,
  activeSectionId: 'start',
  acceleration: 0,
  sectionActivation: {
    layoutRevision: 1,
    activeHeadingOffsetVh: -0.1,
    nextHeadingOffsetVh: 0.8,
  },
  navigation: {
    epoch: 1,
    source: 'passive-scroll',
    phase: 'idle',
    destinationId: null,
    signDestinationId: null,
  },
  ...overrides,
});

Cover passive dwell and reversal:

it('commits once only after 6vh hysteresis and 400 ms dwell', () => {
  const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0);
  expect(coordinator.sample(baseSample({
    now: 10,
    scrollY: 10,
    activeSectionId: 'profile',
    sectionActivation: {
      layoutRevision: 1,
      activeHeadingOffsetVh: -0.059,
      nextHeadingOffsetVh: 0.8,
    },
  })).toMatchObject({ chapter: null, needsFrame: false });

  expect(coordinator.sample(baseSample({ now: 100, scrollY: 10, activeSectionId: 'profile' })))
    .toMatchObject({ chapter: null, needsFrame: true });
  expect(coordinator.sample(baseSample({ now: 499, scrollY: 10, activeSectionId: 'profile' }))).toMatchObject({ chapter: null });
  expect(coordinator.sample(baseSample({ now: 500, scrollY: 10, activeSectionId: 'profile' })).chapter)
    .toMatchObject({ sectionId: 'profile', mood: 'open', generation: 1 });
  expect(coordinator.sample(baseSample({ now: 900, scrollY: 10, activeSectionId: 'profile' })).chapter).toBeNull();
});

it('clears a passive candidate on reversal or structural remeasurement', () => {
  const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0);
  coordinator.sample(baseSample({ now: 0, scrollY: 10, activeSectionId: 'profile' }));
  expect(coordinator.sample(baseSample({ now: 250, scrollY: 9, activeSectionId: 'start' })).needsFrame).toBe(false);
  coordinator.sample(baseSample({ now: 300, scrollY: 12, activeSectionId: 'profile' }));
  expect(coordinator.sample(baseSample({
    now: 800,
    scrollY: 12,
    activeSectionId: 'profile',
    sectionActivation: {
      layoutRevision: 2,
      activeHeadingOffsetVh: -0.2,
      nextHeadingOffsetVh: 0.7,
    },
  })).toMatchObject({ chapter: null, needsFrame: false });
});

Cover rapid explicit navigation and drive classification:

it('commits only the latest settled explicit destination', () => {
  const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0);
  coordinator.sample(baseSample({
    navigation: { epoch: 2, source: 'explicit-navigation', phase: 'travelling', destinationId: 'contact', signDestinationId: null },
  }));
  coordinator.sample(baseSample({
    activeSectionId: 'toolkit',
    navigation: { epoch: 3, source: 'explicit-navigation', phase: 'travelling', destinationId: 'toolkit', signDestinationId: null },
  }));
  expect(coordinator.sample(baseSample({
    now: 600,
    activeSectionId: 'toolkit',
    navigation: { epoch: 3, source: 'explicit-navigation', phase: 'settled', destinationId: 'toolkit', signDestinationId: null },
  })).toMatchObject({
    chapter: { sectionId: 'toolkit', mood: 'resolve', generation: 1 },
  });
});

it('dead-bands tiny input and caps programmatic travel below lift entry', () => {
  const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0);
  expect(coordinator.sample(baseSample({ now: 0, acceleration: 0.07 })).drive).toBeNull();
  expect(coordinator.sample(baseSample({ now: 40, acceleration: 0.7 })).drive)
    .toEqual({ energy: expect.closeTo((0.7 - 0.08) / 0.92), source: 'physical' });
  expect(coordinator.sample(baseSample({
    now: 80,
    acceleration: 1,
    navigation: { epoch: 2, source: 'explicit-navigation', phase: 'travelling', destinationId: 'contact', signDestinationId: null },
  })).drive).toEqual({ energy: 0.35, source: 'navigation' });
  expect(coordinator.sample(baseSample({ now: 120, acceleration: 0 })).drive)
    .toEqual({ energy: 0, source: 'idle' });
});

Add one rate-limit case showing that sub-0.02 changes within 33 ms do not emit, while 0.32, 0.62, source, and final-zero crossings emit immediately. Add these layout/visibility cases:

it('cannot turn a layout-only active-section shift into a passive commit', () => {
  const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0);
  expect(coordinator.sample(baseSample({
    now: 100,
    activeSectionId: 'profile',
    sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7 },
  }))).toMatchObject({ chapter: null, needsFrame: false });
  expect(coordinator.sample(baseSample({
    now: 600,
    activeSectionId: 'profile',
    sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7 },
  })).chapter).toBeNull();

  expect(coordinator.sample(baseSample({
    now: 650,
    scrollY: 2,
    activeSectionId: 'profile',
    sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7 },
  })).needsFrame).toBe(true);
  expect(coordinator.sample(baseSample({
    now: 1_050,
    scrollY: 2,
    activeSectionId: 'profile',
    sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7 },
  })).chapter).toMatchObject({ sectionId: 'profile' });
});

it('defers a genuine settlement across remeasurement but ignores old settled state', () => {
  const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0);
  coordinator.sample(baseSample({
    navigation: { epoch: 2, source: 'explicit-navigation', phase: 'travelling', destinationId: 'contact', signDestinationId: null },
  }));
  expect(coordinator.sample(baseSample({
    now: 100,
    activeSectionId: 'contact',
    sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: null },
    navigation: { epoch: 2, source: 'explicit-navigation', phase: 'settled', destinationId: 'contact', signDestinationId: null },
  }))).toMatchObject({ chapter: null, needsFrame: true });
  expect(coordinator.sample(baseSample({
    now: 116,
    activeSectionId: 'contact',
    sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: null },
    navigation: { epoch: 2, source: 'explicit-navigation', phase: 'settled', destinationId: 'contact', signDestinationId: null },
  })).chapter).toMatchObject({ sectionId: 'contact' });
});

it('starts one final dwell after wheel events advance passive epochs', () => {
  const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0);
  const passive = (epoch: number) => ({
    epoch,
    source: 'passive-scroll' as const,
    phase: 'idle' as const,
    destinationId: null,
    signDestinationId: null,
  });
  coordinator.sample(baseSample({
    now: 100,
    scrollY: 24,
    activeSectionId: 'profile',
    navigation: passive(2),
  }));
  expect(coordinator.sample(baseSample({
    now: 180,
    scrollY: 48,
    activeSectionId: 'profile',
    navigation: passive(3),
  })).needsFrame).toBe(true);
  expect(coordinator.sample(baseSample({
    now: 580,
    scrollY: 48,
    activeSectionId: 'profile',
    navigation: passive(3),
  })).chapter).toMatchObject({ sectionId: 'profile' });
});

it('suppresses a navigation that settled while hidden', () => {
  const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0);
  const settled = {
    epoch: 2,
    source: 'explicit-navigation',
    phase: 'settled',
    destinationId: 'contact',
    signDestinationId: null,
  } as const;
  coordinator.reset({
    navigation: settled,
    scrollY: 0,
    sectionActivation: baseSample().sectionActivation,
  });
  expect(coordinator.sample(baseSample({
    now: 1_000,
    activeSectionId: 'contact',
    navigation: settled,
  })).chapter).toBeNull();
});

Create tests/unit/use-audio-intent.test.ts with a tiny mounted binding and this direct-load assertion:

it('seeds the first measured deep-link section before audio activation', () => {
  const snapshot = shallowRef<JourneySnapshot>({
    scrollY: 7_200,
    routeProgress: 1,
    direction: 0,
    acceleration: 0,
    driveSpeed: 0.18,
    activeSectionId: 'contact',
    chapter: 'destination',
    sign: null,
    arrived: true,
    sectionActivation: {
      layoutRevision: 4,
      activeHeadingOffsetVh: -0.2,
      nextHeadingOffsetVh: null,
    },
  });
  const navigation = shallowRef<NavigationState>({
    epoch: 1,
    source: 'initial',
    phase: 'settled',
    destinationId: 'contact',
    signDestinationId: null,
  });
  let frame: ((now: number, value: JourneySnapshot) => boolean) | null = null;
  const journey: JourneySectionsBinding = {
    snapshot: readonly(snapshot),
    requestFrame: vi.fn(),
    subscribeFrame(listener) {
      frame = listener;
      return () => { frame = null; };
    },
  };
  let binding: AudioIntentBinding | null = null;
  const wrapper = mount(defineComponent({
    setup() {
      binding = useAudioIntent({ canonicalIds: sectionIds, journey, navigation });
      return () => h('div');
    },
  }));

  expect(frame).not.toBeNull();
  frame!(16, snapshot.value);
  expect(binding!.chapter.value).toEqual(resolveChapterIntent('contact'));
  wrapper.unmount();
});
  • Step 2: Run the coordinator test and verify it fails

Run:

npm run test:unit -- tests/unit/audio-intent-coordinator.test.ts tests/unit/use-audio-intent.test.ts

Expected: FAIL because the coordinator, chapter type, and drive type do not exist.

  • Step 3: Make the section-to-music mapping typed and deterministic

Replace the loose string mapping in journey-music-state.ts with:

const JOURNEY_MUSIC = Object.freeze({
  start: { mood: 'open', entryPhrase: 0 },
  profile: { mood: 'open', entryPhrase: 1 },
  experience: { mood: 'shadow', entryPhrase: 2 },
  infrastructure: { mood: 'shadow', entryPhrase: 3 },
  'selected-builds': { mood: 'lift', entryPhrase: 4 },
  'additional-work': { mood: 'lift', entryPhrase: 5 },
  toolkit: { mood: 'resolve', entryPhrase: 6 },
  contact: { mood: 'resolve', entryPhrase: 7 },
} as const satisfies Readonly<Record<
  SectionId,
  { readonly mood: MusicMood; readonly entryPhrase: number }
>>);

export function resolveChapterIntent(
  sectionId: SectionId,
  generation = 0,
): ChapterIntent {
  const chapter = JOURNEY_MUSIC[sectionId];
  return Object.freeze({
    sectionId,
    mood: chapter.mood,
    atStart: sectionId === 'start',
    entryPhrase: chapter.entryPhrase,
    generation: Math.max(0, Math.trunc(generation)),
  });
}

export const DEFAULT_CHAPTER_INTENT = resolveChapterIntent('start');
export const DEFAULT_DRIVE_ENERGY = Object.freeze({
  energy: 0,
  source: 'idle',
} as const satisfies DriveEnergySample);

Keep resolveJourneyMusicState() as a short compatibility adapter through Task 5 so the existing engine continues compiling; Task 6 removes it.

  • Step 4: Implement the pure coordinator

Use these exact constants and public surface in audio-intent-coordinator.ts:

const PASSIVE_HYSTERESIS_VH = 0.06;
const PASSIVE_DWELL_MS = 400;
const DRIVE_DEAD_ZONE = 0.08;
const NAVIGATION_DRIVE_CAP = 0.35;
const DRIVE_EMIT_INTERVAL_MS = 33;
const DRIVE_EMIT_DELTA = 0.02;
const DRIVE_LIFT_EXIT = 0.32;
const DRIVE_LIFT_ENTRY = 0.62;

export interface AudioIntentSample {
  readonly now: number;
  readonly visible: boolean;
  readonly scrollY: number;
  readonly activeSectionId: SectionId;
  readonly acceleration: number;
  readonly sectionActivation: SectionActivationSnapshot;
  readonly navigation: NavigationState;
}

export interface AudioIntentUpdate {
  readonly chapter: ChapterIntent | null;
  readonly drive: DriveEnergySample | null;
  readonly needsFrame: boolean;
}

export interface AudioIntentCoordinator {
  current(): AudioInitialState;
  sample(input: AudioIntentSample): AudioIntentUpdate;
  reset(input: Pick<
    AudioIntentSample,
    'navigation' | 'scrollY' | 'sectionActivation'
  >): AudioIntentUpdate;
}

Implement state and commitment with no timers or DOM access:

export function createAudioIntentCoordinator(
  canonicalIds: readonly SectionId[],
  initialSectionId: SectionId,
  initialLayoutRevision: number,
  initialScrollY: number,
): AudioIntentCoordinator {
  const indexById = new Map(canonicalIds.map((id, index) => [id, index]));
  if (!indexById.has(initialSectionId)) {
    throw new Error(`Unknown initial audio section: ${initialSectionId}`);
  }

  let committedId = initialSectionId;
  let generation = 0;
  let pendingId: SectionId | null = null;
  let pendingSince = 0;
  let layoutRevision = initialLayoutRevision;
  let scrollYAtLayout = initialScrollY;
  let movedSinceLayout = false;
  let navigationEpoch = -1;
  let navigationPhase: NavigationPhase = 'idle';
  let suppressedSettledEpoch = -1;
  let deferredSettlement: {
    readonly epoch: number;
    readonly destinationId: SectionId;
    readonly layoutRevision: number;
  } | null = null;
  let drive: DriveEnergySample = DEFAULT_DRIVE_ENERGY;
  let driveEmittedAt = Number.NEGATIVE_INFINITY;

  const clearCandidate = (): void => {
    pendingId = null;
    pendingSince = 0;
  };
  const commit = (id: SectionId): ChapterIntent | null => {
    if (id === committedId) return null;
    committedId = id;
    generation += 1;
    clearCandidate();
    return resolveChapterIntent(id, generation);
  };
  const current = (): AudioInitialState => ({
    chapter: resolveChapterIntent(committedId, generation),
    drive,
  });

Classify and throttle drive independently:

  const classifyDrive = (input: AudioIntentSample): DriveEnergySample => {
    const acceleration = Math.min(1, Math.max(0, input.acceleration));
    if (!input.visible) return DEFAULT_DRIVE_ENERGY;
    if (input.navigation.phase === 'travelling') {
      return acceleration === 0
        ? DEFAULT_DRIVE_ENERGY
        : { energy: Math.min(NAVIGATION_DRIVE_CAP, acceleration), source: 'navigation' };
    }
    if (input.navigation.phase !== 'idle' || acceleration <= DRIVE_DEAD_ZONE) {
      return DEFAULT_DRIVE_ENERGY;
    }
    return {
      energy: (acceleration - DRIVE_DEAD_ZONE) / (1 - DRIVE_DEAD_ZONE),
      source: 'physical',
    };
  };

  const crossed = (previous: number, next: number, threshold: number): boolean =>
    (previous < threshold && next >= threshold) ||
    (previous >= threshold && next < threshold);

  const emitDrive = (
    next: DriveEnergySample,
    now: number,
  ): DriveEnergySample | null => {
    const urgent = next.source !== drive.source ||
      (next.energy === 0) !== (drive.energy === 0) ||
      crossed(drive.energy, next.energy, DRIVE_LIFT_EXIT) ||
      crossed(drive.energy, next.energy, DRIVE_LIFT_ENTRY);
    const due = now - driveEmittedAt >= DRIVE_EMIT_INTERVAL_MS &&
      Math.abs(next.energy - drive.energy) >= DRIVE_EMIT_DELTA;
    if (!urgent && !due) return null;
    drive = Object.freeze(next);
    driveEmittedAt = now;
    return drive;
  };

Complete sample() with edge-triggered settlement, a one-frame measurement-order bridge, and a movement requirement after every layout revision. This lets a real settlement survive a same-frame remeasurement while preventing a stale settled snapshot or a resize-only active-section shift from committing:

  const suppressCurrentPosition = (input: Pick<
    AudioIntentSample,
    'navigation' | 'scrollY' | 'sectionActivation'
  >): void => {
    clearCandidate();
    deferredSettlement = null;
    layoutRevision = input.sectionActivation.layoutRevision;
    scrollYAtLayout = input.scrollY;
    movedSinceLayout = false;
    navigationEpoch = input.navigation.epoch;
    navigationPhase = input.navigation.phase;
    suppressedSettledEpoch = input.navigation.epoch;
  };

  const sample = (input: AudioIntentSample): AudioIntentUpdate => {
    let chapter: ChapterIntent | null = null;
    const nextDrive = emitDrive(classifyDrive(input), input.now);
    if (!input.visible) {
      suppressCurrentPosition(input);
      return { chapter: null, drive: nextDrive, needsFrame: false };
    }

    const layoutChanged = input.sectionActivation.layoutRevision !== layoutRevision;
    if (layoutChanged) {
      layoutRevision = input.sectionActivation.layoutRevision;
      scrollYAtLayout = input.scrollY;
      movedSinceLayout = false;
      clearCandidate();
      deferredSettlement = null;
    }
    if (Math.abs(input.scrollY - scrollYAtLayout) >= 1) movedSinceLayout = true;

    const navigationChanged = input.navigation.epoch !== navigationEpoch;
    const settledEdge = input.navigation.phase === 'settled' &&
      (navigationChanged || navigationPhase !== 'settled');
    if (navigationChanged) {
      navigationEpoch = input.navigation.epoch;
      clearCandidate();
      deferredSettlement = null;
    }
    navigationPhase = input.navigation.phase;

    if (input.navigation.phase === 'travelling') {
      return { chapter: null, drive: nextDrive, needsFrame: false };
    }

    const destination = input.navigation.destinationId;
    if (layoutChanged) {
      if (
        settledEdge &&
        input.navigation.epoch !== suppressedSettledEpoch &&
        destination !== null
      ) {
        deferredSettlement = {
          epoch: input.navigation.epoch,
          destinationId: destination,
          layoutRevision,
        };
        return { chapter: null, drive: nextDrive, needsFrame: true };
      }
      return { chapter: null, drive: nextDrive, needsFrame: false };
    }

    const deferred = deferredSettlement;
    if (deferred !== null) {
      deferredSettlement = null;
      const stillValid = input.navigation.phase === 'settled' &&
        input.navigation.epoch === deferred.epoch &&
        input.navigation.epoch !== suppressedSettledEpoch &&
        input.navigation.destinationId === deferred.destinationId &&
        input.sectionActivation.layoutRevision === deferred.layoutRevision &&
        input.activeSectionId === deferred.destinationId;
      chapter = stillValid ? commit(deferred.destinationId) : null;
      return { chapter, drive: nextDrive, needsFrame: false };
    }

    if (input.navigation.phase === 'settled') {
      if (
        settledEdge &&
        input.navigation.epoch !== suppressedSettledEpoch &&
        destination !== null &&
        destination === input.activeSectionId
      ) {
        chapter = commit(destination);
      } else if (
        settledEdge &&
        input.navigation.epoch !== suppressedSettledEpoch &&
        destination !== null
      ) {
        deferredSettlement = {
          epoch: input.navigation.epoch,
          destinationId: destination,
          layoutRevision,
        };
        return { chapter: null, drive: nextDrive, needsFrame: true };
      }
      return { chapter, drive: nextDrive, needsFrame: false };
    }

    if (!movedSinceLayout || input.activeSectionId === committedId) {
      clearCandidate();
      return { chapter: null, drive: nextDrive, needsFrame: false };
    }

    const committedIndex = indexById.get(committedId);
    const candidateIndex = indexById.get(input.activeSectionId);
    if (committedIndex === undefined || candidateIndex === undefined) {
      throw new Error('Audio intent received a non-canonical section.');
    }
    const eligible = candidateIndex > committedIndex
      ? input.sectionActivation.activeHeadingOffsetVh <= -PASSIVE_HYSTERESIS_VH
      : input.sectionActivation.nextHeadingOffsetVh !== null &&
        input.sectionActivation.nextHeadingOffsetVh >= PASSIVE_HYSTERESIS_VH;
    if (!eligible) {
      clearCandidate();
      return { chapter: null, drive: nextDrive, needsFrame: false };
    }
    if (pendingId !== input.activeSectionId) {
      pendingId = input.activeSectionId;
      pendingSince = input.now;
      return { chapter: null, drive: nextDrive, needsFrame: true };
    }
    if (input.now - pendingSince < PASSIVE_DWELL_MS) {
      return { chapter: null, drive: nextDrive, needsFrame: true };
    }
    chapter = commit(input.activeSectionId);
    return { chapter, drive: nextDrive, needsFrame: false };
  };

  return {
    current,
    sample,
    reset(input) {
      suppressCurrentPosition(input);
      const nextDrive = drive.energy === 0 && drive.source === 'idle'
        ? null
        : DEFAULT_DRIVE_ENERGY;
      drive = DEFAULT_DRIVE_ENERGY;
      driveEmittedAt = Number.NEGATIVE_INFINITY;
      return { chapter: null, drive: nextDrive, needsFrame: false };
    },
  };
}
  • Step 5: Bridge the coordinator to Vue's shared journey frame

Create useAudioIntent.ts with this contract:

export interface AudioIntentSources {
  readonly canonicalIds: readonly SectionId[];
  readonly journey: JourneySectionsBinding;
  readonly navigation: Readonly<Ref<NavigationState>>;
}

export interface AudioIntentBinding {
  readonly chapter: Readonly<Ref<ChapterIntent>>;
  readonly drive: Readonly<Ref<DriveEnergySample>>;
}

Expose a safe Start fallback during hydration, then seed the coordinator from the first measured journey snapshot before processing deltas. This makes a direct #contact load select Contact rather than replaying Start state. Subscribe after journey measurement and request the same frame clock during dwell:

export function useAudioIntent(sources: AudioIntentSources): AudioIntentBinding {
  const firstId = sources.canonicalIds[0];
  if (firstId === undefined) throw new Error('Audio intent requires one section.');
  const chapter = shallowRef(resolveChapterIntent(firstId));
  const drive = shallowRef(DEFAULT_DRIVE_ENERGY);
  let coordinator: AudioIntentCoordinator | null = null;
  let removeFrame = (): void => {};

  const publish = (update: AudioIntentUpdate): boolean => {
    if (update.chapter !== null) chapter.value = update.chapter;
    if (update.drive !== null) drive.value = update.drive;
    return update.needsFrame;
  };
  const sample = (now: number, snapshot: JourneySnapshot): boolean => {
    if (coordinator === null) {
      coordinator = createAudioIntentCoordinator(
        sources.canonicalIds,
        snapshot.activeSectionId,
        snapshot.sectionActivation.layoutRevision,
        snapshot.scrollY,
      );
      const seeded = coordinator.current();
      chapter.value = seeded.chapter;
      drive.value = seeded.drive;
    }
    return publish(coordinator.sample({
      now,
      visible: document.visibilityState !== 'hidden',
      scrollY: snapshot.scrollY,
      activeSectionId: snapshot.activeSectionId,
      acceleration: snapshot.acceleration,
      sectionActivation: snapshot.sectionActivation,
      navigation: sources.navigation.value,
    }));
  };

  const stopNavigationWatch = watch(
    sources.navigation,
    () => sources.journey.requestFrame(),
    { flush: 'sync' },
  );
  onMounted(() => {
    removeFrame = sources.journey.subscribeFrame(sample);
    const resetFromCurrentSnapshot = (): void => {
      if (coordinator === null) return;
      const snapshot = sources.journey.snapshot.value;
      publish(coordinator.reset({
        navigation: sources.navigation.value,
        scrollY: snapshot.scrollY,
        sectionActivation: snapshot.sectionActivation,
      }));
    };
    const onVisibility = (): void => {
      resetFromCurrentSnapshot();
      if (document.visibilityState !== 'hidden') sources.journey.requestFrame();
    };
    document.addEventListener('visibilitychange', onVisibility);
    removeFrame = combineCleanup(removeFrame, () =>
      document.removeEventListener('visibilitychange', onVisibility));
    sources.journey.requestFrame();
  });
  onUnmounted(() => {
    stopNavigationWatch();
    removeFrame();
  });
  return { chapter: readonly(chapter), drive: readonly(drive) };
}

function combineCleanup(left: () => void, right: () => void): () => void {
  return () => { left(); right(); };
}

Use Vue imports for onMounted, onUnmounted, readonly, shallowRef, watch, and Ref. This module performs no DOM measurement and creates no timer.

  • Step 6: Run the focused coordinator and journey checks

Run:

npm run test:unit -- tests/unit/audio-intent-coordinator.test.ts tests/unit/use-audio-intent.test.ts tests/unit/navigation-controller.test.ts tests/unit/section-controller.test.ts
npm run typecheck

Expected: all checks pass; no existing visual section behavior changes; coordinator emissions are stable and deterministic.

  • Step 7: Commit the coordinator

Run:

git add src/audio/types.ts src/audio/journey-music-state.ts src/audio/audio-intent-coordinator.ts src/composables/useAudioIntent.ts tests/unit/audio-intent-coordinator.test.ts tests/unit/use-audio-intent.test.ts
git commit -m "feat: coordinate adaptive audio intent"

Expected: one pure state-management commit with no Web Audio or asset changes.

Task 6: Split controller commands and wire stable intent into the app

Files:

  • Modify: src/audio/types.ts:9-65
  • Modify: src/audio/create-audio-controller.ts:1-300
  • Modify: src/audio/journey-music-state.ts
  • Modify: src/audio/soundtrack-transport.ts:1-38
  • Modify: src/audio/soundtrack-engine.ts:1-335
  • Modify: src/composables/useAudioEngine.ts:1-104
  • Modify: src/components/AppShell.vue:1-111
  • Modify: tests/unit/audio-lifecycle.test.ts:1-286
  • Modify: tests/unit/soundtrack-transport.test.ts:1-51

Interfaces:

  • Consumes: AudioIntentBinding.chapter, AudioIntentBinding.drive, the four-region soundtrack content contract, and the existing lifecycle controller.
  • Produces: AudioController.setChapterIntent(), AudioController.setDriveEnergy(), an AudioGraphFactory(AudioInitialState), and independent sync watches. The current master remains audible while later tasks add the two adaptive buses.

The final public audio command boundary is:

export interface AudioGraph {
  resume(): Promise<void>;
  suspend(): Promise<void>;
  close(): Promise<void>;
  connectOutput(): void;
  disconnectOutput(): void;
  setOutputGain(value: number): void;
  setChapterIntent(intent: ChapterIntent): void;
  setDriveEnergy(sample: DriveEnergySample): void;
  cancelPending(): void;
  onContextStateChange(
    listener: (state: AudioContextLifecycleState) => void,
  ): () => void;
}

export type AudioGraphFactory = (initialState: AudioInitialState) => AudioGraph;
  • Step 1: Replace the combined lifecycle test with independent-command tests

In tests/unit/audio-lifecycle.test.ts, replace the combined-state case with:

it('stores and normalizes chapter and drive independently before activation', async () => {
  const harness = createHarness();
  const controller = createAudioController({ createGraph: harness.createGraph });
  controller.setChapterIntent({
    sectionId: 'contact',
    mood: 'resolve',
    atStart: false,
    entryPhrase: 99,
    generation: -3,
  });
  controller.setDriveEnergy({ energy: 2, source: 'navigation' });

  expect(harness.constructed).toBe(0);
  await controller.startFromGesture();
  expect(harness.initialStates).toEqual([{
    chapter: {
      sectionId: 'contact',
      mood: 'resolve',
      atStart: false,
      entryPhrase: 7,
      generation: 0,
    },
    drive: { energy: 1, source: 'navigation' },
  }]);

  controller.setChapterIntent({
    sectionId: 'experience',
    mood: 'shadow',
    atStart: false,
    entryPhrase: 2,
    generation: 4,
  });
  controller.setDriveEnergy({ energy: 0.73, source: 'physical' });
  expect(harness.graph.setChapterIntent).toHaveBeenLastCalledWith({
    sectionId: 'experience',
    mood: 'shadow',
    atStart: false,
    entryPhrase: 2,
    generation: 4,
  });
  expect(harness.graph.setDriveEnergy).toHaveBeenLastCalledWith({
    energy: 0.73,
    source: 'physical',
  });
});

Change the harness to use setChapterIntent: vi.fn() and setDriveEnergy: vi.fn(), and capture AudioInitialState[] from the factory.

  • Step 2: Run the lifecycle and transport tests and verify they fail

Run:

npm run test:unit -- tests/unit/audio-lifecycle.test.ts tests/unit/soundtrack-transport.test.ts

Expected: FAIL because controller and graph still expose only setJourneyState().

  • Step 3: Finalize the independent audio types

Remove AudioJourneyState and the compatibility MusicChapterBias alias. Set the source and controller contracts to:

export interface SoundtrackSources {
  readonly intro: readonly AudioSourceVariant[];
  readonly main: readonly AudioSourceVariant[];
  readonly afterglow: readonly AudioSourceVariant[];
  readonly driveLift: readonly AudioSourceVariant[];
}

export interface AudioControllerOptions {
  readonly createGraph: AudioGraphFactory;
  readonly initialVolume?: number;
  readonly initiallyVisible?: boolean;
  readonly initialState?: AudioInitialState;
}

export interface AudioController {
  startFromGesture(): Promise<void>;
  resumeFromGesture(): Promise<void>;
  setMuted(muted: boolean): void;
  setVolume(volume: number): void;
  setChapterIntent(intent: ChapterIntent): void;
  setDriveEnergy(sample: DriveEnergySample): void;
  setVisible(visible: boolean): Promise<void>;
  noteInterruption(): Promise<void>;
  pageHide(persisted: boolean): Promise<void>;
  pageShow(persisted: boolean): Promise<void>;
  dispose(): void;
  whenSettled(): Promise<void>;
  snapshot(): AudioUiState;
  subscribe(listener: (state: AudioUiState) => void): () => void;
}

Add DEFAULT_AUDIO_INITIAL_STATE in journey-music-state.ts:

export const DEFAULT_AUDIO_INITIAL_STATE = Object.freeze({
  chapter: DEFAULT_CHAPTER_INTENT,
  drive: DEFAULT_DRIVE_ENERGY,
} as const satisfies AudioInitialState);
  • Step 4: Cache and forward both commands without changing lifecycle ownership

In create-audio-controller.ts, replace journeyState with:

let chapterIntent = normalizeChapterIntent(
  options.initialState?.chapter ?? DEFAULT_AUDIO_INITIAL_STATE.chapter,
);
let driveEnergy = normalizeDriveEnergy(
  options.initialState?.drive ?? DEFAULT_AUDIO_INITIAL_STATE.drive,
);

Keep graph creation synchronous in the gesture stack:

graph = options.createGraph({ chapter: chapterIntent, drive: driveEnergy });
removeStateListener = graph.onContextStateChange((state) => {
  if (state === 'interrupted') void noteInterruption();
});
graph.setOutputGain(0);
graph.setChapterIntent(chapterIntent);
graph.setDriveEnergy(driveEnergy);

When attaching an intact graph, reapply both latest values:

graph.connectOutput();
graph.setOutputGain(muted ? 0 : volume);
graph.setChapterIntent(chapterIntent);
graph.setDriveEnergy(driveEnergy);

Expose independent setters:

setChapterIntent(nextIntent) {
  if (actual === 'disposed') return;
  chapterIntent = normalizeChapterIntent(nextIntent);
  graph?.setChapterIntent(chapterIntent);
},
setDriveEnergy(nextSample) {
  if (actual === 'disposed') return;
  driveEnergy = normalizeDriveEnergy(nextSample);
  graph?.setDriveEnergy(driveEnergy);
},

Use these normalization functions:

function normalizeChapterIntent(intent: ChapterIntent): ChapterIntent {
  const entryPhrase = Number.isFinite(intent.entryPhrase)
    ? Math.trunc(intent.entryPhrase)
    : 0;
  const generation = Number.isFinite(intent.generation)
    ? Math.trunc(intent.generation)
    : 0;
  return Object.freeze({
    sectionId: intent.sectionId,
    mood: intent.mood,
    atStart: intent.atStart === true,
    entryPhrase: Math.min(7, Math.max(0, entryPhrase)),
    generation: Math.max(0, generation),
  });
}

function normalizeDriveEnergy(sample: DriveEnergySample): DriveEnergySample {
  return Object.freeze({ energy: clamp(sample.energy), source: sample.source });
}

Do not alter gesture, visibility, interruption, mute, volume, BFCache, failure, or teardown branches.

  • Step 5: Adapt current transport and engine to the split contract

Change resolveInitialPlayback to accept ChapterIntent. In soundtrack-engine.ts, accept AudioInitialState, replace latestState with latestChapter, read initialState.chapter.atStart, and apply mood from chapter.mood. Expose:

setChapterIntent(intent) {
  if (closed) return;
  latestChapter = intent;
  applyMix(intent);
  if (!intent.atStart) requestIntroExit();
  else cancelUnscheduledIntroExit();
},
setDriveEnergy(_sample) {
  if (closed) return;
},

Define the compatibility cancellation helper beside requestIntroExit():

const cancelUnscheduledIntroExit = (): void => {
  if (exitTimer === undefined) return;
  window.clearTimeout(exitTimer);
  exitTimer = undefined;
};

The no-op drive setter is temporary but intentional: it proves that continuous drive frames cannot retarget chapter mix while Tasks 7-9 add the dedicated path. Retain all existing intro/main behavior in this task.

  • Step 6: Make the Vue engine binding watch the two refs independently

Replace AudioEngineSources with:

export interface AudioEngineSources {
  readonly chapter: Readonly<Ref<ChapterIntent>>;
  readonly drive: Readonly<Ref<DriveEnergySample>>;
  readonly soundtrack: Readonly<Ref<SoundtrackSources | null>>;
  readonly volume: Readonly<Ref<number>>;
  setVolume(value: number): void;
}

Construct and watch with:

controller = createAudioController({
  createGraph: (initialState) => {
    const soundtrack = sources.soundtrack.value;
    if (soundtrack === null) throw new Error('Soundtrack assets are unavailable.');
    return createBrowserSoundtrackEngine(soundtrack, initialState);
  },
  initialVolume: sources.volume.value,
  initiallyVisible: document.visibilityState !== 'hidden',
  initialState: {
    chapter: sources.chapter.value,
    drive: sources.drive.value,
  },
});
watch(sources.chapter, (intent) => controller?.setChapterIntent(intent), { flush: 'sync' });
watch(sources.drive, (sample) => controller?.setDriveEnergy(sample), { flush: 'sync' });
watch(sources.volume, (value) => controller?.setVolume(value));
  • Step 7: Remove the eager destination shortcut and wire stable intent

In AppShell.vue, delete pendingAudioDestination, audioNavigationEpoch, their watcher, and audioSectionId. Create:

const audioIntent = useAudioIntent({
  canonicalIds: canonicalSectionIds,
  journey: journeyBinding,
  navigation: navigation.state,
});

Resolve every soundtrack region:

const variants = (
  sources: { readonly opusAssetId: string; readonly mp3AssetId: string },
): readonly AudioSourceVariant[] => [
  { url: assetUrl(props.manifest, sources.opusAssetId), mediaType: 'audio/ogg' },
  { url: assetUrl(props.manifest, sources.mp3AssetId), mediaType: 'audio/mpeg' },
];

const soundtrackSources = computed<SoundtrackSources | null>(() => {
  const soundtrack = props.content.soundtrack;
  if (soundtrack === undefined) return null;
  return {
    intro: variants(soundtrack.intro),
    main: variants(soundtrack.main),
    afterglow: variants(soundtrack.afterglow),
    driveLift: variants(soundtrack.driveLift),
  };
});

const audio = useAudioEngine({
  chapter: audioIntent.chapter,
  drive: audioIntent.drive,
  soundtrack: soundtrackSources,
  volume: motion.volume,
  setVolume: motion.setVolume,
});

Import useAudioIntent and AudioSourceVariant; remove now-unused watch or shallowRef imports only when no other caller uses them.

  • Step 8: Run the focused lifecycle, intent, and type checks

Run:

npm run test:unit -- tests/unit/audio-lifecycle.test.ts tests/unit/soundtrack-transport.test.ts tests/unit/audio-intent-coordinator.test.ts tests/unit/use-audio-intent.test.ts tests/unit/navigation-controller.test.ts tests/unit/section-controller.test.ts
npm run typecheck

Expected: all checks pass. The master still plays, but continuous drive energy no longer touches its chapter gain/filter path.

  • Step 9: Commit the split command boundary

Run:

git add src/audio/types.ts src/audio/create-audio-controller.ts src/audio/journey-music-state.ts src/audio/soundtrack-transport.ts src/audio/soundtrack-engine.ts src/composables/useAudioEngine.ts src/components/AppShell.vue tests/unit/audio-lifecycle.test.ts tests/unit/soundtrack-transport.test.ts
git commit -m "refactor: split soundtrack chapter and drive commands"

Expected: a lifecycle-preserving refactor with stable intent wired end to end.

Task 7: Restore the restrained drive texture as an isolated module

Files:

  • Create: tests/helpers/fake-audio-context.ts
  • Create: src/audio/drive-texture.ts
  • Create: tests/unit/drive-texture.test.ts

Interfaces:

  • Consumes: the soundtrack engine's existing AudioContext, one destination bus, and normalized DriveEnergySample commands.
  • Produces: one deterministic, preallocated DriveTexture whose cruise bed is always quiet, whose targets move over 220/900 ms, and whose owned nodes dispose exactly once.
export interface DriveTexture {
  setEnergy(sample: DriveEnergySample): void;
  dispose(): void;
}

export type DriveTextureFactory = (
  context: AudioContext,
  destination: AudioNode,
) => DriveTexture;
  • Step 1: Build the minimal call-recording Web Audio test helper

Create a fake that records scheduling rather than emulating sound. Its public records must be:

export type AudioParamEvent =
  | { readonly kind: 'set'; readonly value: number; readonly at: number }
  | { readonly kind: 'linear'; readonly value: number; readonly at: number }
  | { readonly kind: 'exponential'; readonly value: number; readonly at: number }
  | { readonly kind: 'target'; readonly value: number; readonly at: number; readonly constant: number }
  | { readonly kind: 'cancel'; readonly at: number }
  | { readonly kind: 'hold'; readonly at: number };

export interface FakeAudioParamRecord {
  readonly param: AudioParam;
  readonly events: AudioParamEvent[];
}

export interface FakeSourceRecord {
  readonly kind: 'buffer' | 'oscillator';
  readonly node: AudioScheduledSourceNode;
  readonly starts: Array<{ at: number; offset: number; duration: number | null }>;
  readonly stops: number[];
  disconnectCount: number;
  emitEnded(): void;
}

export interface FakeAudioHarness {
  readonly context: AudioContext;
  readonly params: FakeAudioParamRecord[];
  readonly sources: FakeSourceRecord[];
  readonly gainNodes: GainNode[];
  readonly buffers: AudioBuffer[];
  readonly destination: AudioNode;
  setCurrentTime(seconds: number): void;
  decodedBuffer(durationSeconds: number, channels?: number): AudioBuffer;
  emitContextState(state: AudioContextState | 'interrupted'): void;
}

Use a small parameter factory that updates .value and records every supported event:

const createParam = (
  records: FakeAudioParamRecord[],
  initial = 0,
): AudioParam => {
  const events: AudioParamEvent[] = [];
  const parameter = {
    value: initial,
    defaultValue: initial,
    minValue: Number.NEGATIVE_INFINITY,
    maxValue: Number.POSITIVE_INFINITY,
    automationRate: 'a-rate' as const,
    setValueAtTime(value: number, at: number) {
      this.value = value;
      events.push({ kind: 'set', value, at });
      return this as unknown as AudioParam;
    },
    linearRampToValueAtTime(value: number, at: number) {
      this.value = value;
      events.push({ kind: 'linear', value, at });
      return this as unknown as AudioParam;
    },
    exponentialRampToValueAtTime(value: number, at: number) {
      this.value = value;
      events.push({ kind: 'exponential', value, at });
      return this as unknown as AudioParam;
    },
    setTargetAtTime(value: number, at: number, constant: number) {
      this.value = value;
      events.push({ kind: 'target', value, at, constant });
      return this as unknown as AudioParam;
    },
    cancelScheduledValues(at: number) {
      events.push({ kind: 'cancel', at });
      return this as unknown as AudioParam;
    },
    cancelAndHoldAtTime(at: number) {
      events.push({ kind: 'hold', at });
      return this as unknown as AudioParam;
    },
    setValueCurveAtTime() {
      throw new Error('Unexpected value-curve scheduling in focused audio tests.');
    },
  } as unknown as AudioParam;
  records.push({ param: parameter, events });
  return parameter;
};

Create lightweight gain, filter, panner, buffer-source, oscillator, buffer, and destination objects with connect() and counted disconnect(). Push every created gain into gainNodes and every created buffer into buffers in creation order. start() records default at = 0, offset = 0, and duration = null; stop() records once per call; emitEnded() invokes registered one-shot ended listeners. The fake context must expose only the methods used by drive-texture.ts, adaptive-mixer.ts, and soundtrack-engine.ts: createGain, createBiquadFilter, createStereoPanner, createBuffer, createBufferSource, createOscillator, decodeAudioData, resume, suspend, close, addEventListener, and removeEventListener. Throw on any unexpected method so tests reveal graph expansion.

  • Step 2: Write failing drive-texture tests

Create tests/unit/drive-texture.test.ts:

it('preallocates one texture and uses exact attack and release ramps', () => {
  const harness = createFakeAudioHarness();
  const texture = createDriveTexture(harness.context, harness.destination);
  const sourceCount = harness.sources.length;
  const eventCount = () => harness.params.flatMap((record) => record.events).length;

  harness.setCurrentTime(2);
  const beforeAttack = eventCount();
  texture.setEnergy({ energy: 1, source: 'physical' });
  expect(harness.sources).toHaveLength(sourceCount);
  expect(harness.params.flatMap((record) => record.events).slice(beforeAttack))
    .toContainEqual(expect.objectContaining({ kind: 'linear', at: 2.22 }));

  harness.setCurrentTime(3);
  const beforeRelease = eventCount();
  texture.setEnergy({ energy: 0, source: 'idle' });
  expect(harness.sources).toHaveLength(sourceCount);
  expect(harness.params.flatMap((record) => record.events).slice(beforeRelease))
    .toContainEqual(expect.objectContaining({ kind: 'linear', at: 3.9 }));
});

it('stops and disconnects every scheduled source exactly once', () => {
  const harness = createFakeAudioHarness();
  const texture = createDriveTexture(harness.context, harness.destination);
  texture.dispose();
  texture.dispose();
  for (const source of harness.sources) {
    expect(source.stops).toHaveLength(1);
    expect(source.disconnectCount).toBe(1);
  }
});

Add one test that constructs two harnesses and asserts their two-channel noise buffers contain identical samples for the fixed seed while left and right channels differ from each other.

  • Step 3: Run the texture test and verify it fails

Run:

npm run test:unit -- tests/unit/drive-texture.test.ts

Expected: FAIL because neither the fake context nor createDriveTexture exists.

  • Step 4: Implement deterministic stereo noise and reusable source helpers

Use a fixed local generator; do not call Math.random():

const NOISE_SECONDS = 2;
const NOISE_SEED = 0x4e494748;

function createNoiseBuffer(
  context: BaseAudioContext,
  seed: number,
): AudioBuffer {
  const length = Math.max(1, Math.round(context.sampleRate * NOISE_SECONDS));
  const buffer = context.createBuffer(2, length, context.sampleRate);
  let state = seed >>> 0;
  for (let channel = 0; channel < 2; channel += 1) {
    const samples = buffer.getChannelData(channel);
    let previous = 0;
    for (let index = 0; index < samples.length; index += 1) {
      state = (Math.imul(state, 1_664_525) + 1_013_904_223) >>> 0;
      const white = state / 0xffff_ffff * 2 - 1;
      previous = previous * 0.9 + white * 0.1;
      samples[index] = previous;
    }
  }
  return buffer;
}

function createOscillator(
  context: AudioContext,
  type: OscillatorType,
  frequency: number,
  level: number,
  destination: AudioNode,
): { readonly source: OscillatorNode; readonly gain: GainNode } {
  const source = context.createOscillator();
  const gain = context.createGain();
  source.type = type;
  source.frequency.setValueAtTime(frequency, context.currentTime);
  gain.gain.setValueAtTime(level, context.currentTime);
  source.connect(gain);
  gain.connect(destination);
  return { source, gain };
}
  • Step 5: Build the restrained fixed graph and exact response envelope

Use these conservative targets in createDriveTexture:

const ATTACK_SECONDS = 0.22;
const RELEASE_SECONDS = 0.9;
const CRUISE_ENGINE_GAIN = 0.055;
const MAX_ENGINE_GAIN = 0.12;
const CRUISE_ROAD_GAIN = 0.006;
const MAX_ROAD_GAIN = 0.024;

Construct once:

const engineFilter = context.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 240;
engineFilter.Q.value = 1.1;
const engineGain = context.createGain();
engineGain.gain.value = CRUISE_ENGINE_GAIN;
engineFilter.connect(engineGain);
engineGain.connect(destination);

const fundamental = createOscillator(context, 'sawtooth', 42, 0.1, engineFilter);
const harmonic = createOscillator(context, 'sine', 84, 0.035, engineFilter);

const roadSource = context.createBufferSource();
roadSource.buffer = createNoiseBuffer(context, NOISE_SEED);
roadSource.loop = true;
const roadHighpass = context.createBiquadFilter();
roadHighpass.type = 'highpass';
roadHighpass.frequency.value = 180;
const roadLowpass = context.createBiquadFilter();
roadLowpass.type = 'lowpass';
roadLowpass.frequency.value = 1_600;
const roadGain = context.createGain();
roadGain.gain.value = CRUISE_ROAD_GAIN;
roadSource.connect(roadHighpass);
roadHighpass.connect(roadLowpass);
roadLowpass.connect(roadGain);
roadGain.connect(destination);

The stereo buffer supplies decorrelated channels; do not collapse it before the filters. Add a short triangle ignition through the engine filter, peaking at 0.008 after 120 ms and returning to 0.0001 by 1.15 seconds. Start the two engine oscillators, the looping road source, and ignition exactly once.

Drive automation uses a single helper:

let currentEnergy = 0;
const automate = (
  parameter: AudioParam,
  value: number,
  duration: number,
): void => {
  const now = context.currentTime;
  holdParameter(parameter, now);
  parameter.linearRampToValueAtTime(value, now + duration);
};

const setEnergy = (sample: DriveEnergySample): void => {
  if (disposed) return;
  const next = Math.min(1, Math.max(0, sample.energy));
  const duration = next > currentEnergy ? ATTACK_SECONDS : RELEASE_SECONDS;
  automate(fundamental.source.frequency, 42 + next * 20, duration);
  automate(harmonic.source.frequency, 84 + next * 40, duration);
  automate(engineFilter.frequency, 240 + next * 460, duration);
  automate(roadLowpass.frequency, 1_600 + next * 1_800, duration);
  automate(engineGain.gain, CRUISE_ENGINE_GAIN + next * (MAX_ENGINE_GAIN - CRUISE_ENGINE_GAIN), duration);
  automate(roadGain.gain, CRUISE_ROAD_GAIN + next * (MAX_ROAD_GAIN - CRUISE_ROAD_GAIN), duration);
  currentEnergy = next;
};

Reuse the holdParameter fallback used by the soundtrack engine. Keep every node in an ownership array. dispose() must guard with disposed, stop each scheduled source inside try/catch, disconnect each source and processing node once, and never close the shared context.

  • Step 6: Run the focused texture test and typecheck

Run:

npm run test:unit -- tests/unit/drive-texture.test.ts
npm run typecheck

Expected: both pass; repeated energy commands add no nodes; attack/release timing and exact-once cleanup are recorded.

  • Step 7: Commit the drive texture

Run:

git add tests/helpers/fake-audio-context.ts src/audio/drive-texture.ts tests/unit/drive-texture.test.ts
git commit -m "feat: restore restrained drive texture"

Expected: one independent DSP module with no connection to the live graph yet.

Task 8: Add exact transport math and the adaptive mixer

Files:

  • Modify: src/audio/soundtrack-transport.ts
  • Create: src/audio/adaptive-mixer.ts
  • Modify: tests/helpers/fake-audio-context.ts
  • Modify: tests/unit/soundtrack-transport.test.ts
  • Create: tests/unit/adaptive-mixer.test.ts

Interfaces:

  • Consumes: the main transport anchor, ChapterIntent, DriveEnergySample, one shared AudioContext, and conservative authored-layer gain assumptions.
  • Produces: exact sample-derived schedule helpers and AdaptiveMixer input buses with 1.4-second mood ramps and cancellable beat-aligned drive-lift automation.
export interface AdaptiveMixer {
  readonly bedInput: GainNode;
  readonly driveLiftInput: GainNode;
  readonly driveTextureInput: GainNode;
  setChapterIntent(intent: ChapterIntent): void;
  setDriveEnergy(sample: DriveEnergySample): void;
  setTransportAnchor(anchorTime: number | null): void;
  setDriveLiftAvailableAt(startTime: number | null): void;
  dispose(): void;
}

export type AdaptiveMixerFactory = (
  context: AudioContext,
  destination: AudioNode,
) => AdaptiveMixer;
  • Step 1: Write failing exact-timeline tests

Extend tests/unit/soundtrack-transport.test.ts:

it('pins the short adaptive regions to whole samples', () => {
  expect(MUSIC_BEAT_SAMPLES).toBe(33_488);
  expect(MUSIC_BAR_SAMPLES).toBe(133_953);
  expect(DRIVE_LIFT_LOOP_SAMPLES).toBe(535_814);
  expect(AFTERGLOW_LOOP_START_SAMPLE).toBe(133_953);
  expect(AFTERGLOW_LOOP_END_SAMPLE).toBe(1_205_581);
  expect(MAIN_DURATION_SAMPLES).toBe(8_573_023);
});

it('derives one immutable main anchor and the final-bar afterglow handoff', () => {
  expect(resolveMainSchedule({
    startTime: 10,
    offsetSeconds: mainPhraseOffsetSeconds(6),
    bufferDuration: MAIN_DURATION_SECONDS,
  })).toEqual({
    sourceStartTime: 10,
    sourceOffsetSeconds: mainPhraseOffsetSeconds(6),
    musicalAnchorTime: 10 - mainPhraseOffsetSeconds(6),
    sourceEndTime: 10 + MAIN_DURATION_SECONDS - mainPhraseOffsetSeconds(6),
    afterglowStartTime: 10 + MAIN_DURATION_SECONDS - mainPhraseOffsetSeconds(6) - AFTERGLOW_TRANSITION_SECONDS,
  });
});

it('aligns a late loop source and sustained entry to the master beat', () => {
  expect(phaseAlignedLoopOffset(13, 10, DRIVE_LIFT_LOOP_SECONDS))
    .toBeCloseTo(3, 8);
  expect(nextBeatTime(11.45, 10)).toBeCloseTo(
    10 + 3 * MUSIC_BEAT_SECONDS,
    8,
  );
});
  • Step 2: Write failing mixer tests

Expose gainNodes: readonly GainNode[] from the fake harness in creation order. Then create tests/unit/adaptive-mixer.test.ts:

it('ramps only the four approved mood controls over 1.4 seconds', () => {
  const harness = createFakeAudioHarness();
  const mixer = createAdaptiveMixer(harness.context, harness.destination);
  harness.setCurrentTime(2);
  mixer.setChapterIntent({
    sectionId: 'toolkit',
    mood: 'resolve',
    atStart: false,
    entryPhrase: 6,
    generation: 1,
  });
  const events = harness.params.flatMap((record) => record.events);
  expect(events.filter((event) => event.kind === 'linear' && event.at === 3.4))
    .toHaveLength(4);
});

it('arms lift after sustained physical input but never for navigation', () => {
  const harness = createFakeAudioHarness();
  const mixer = createAdaptiveMixer(harness.context, harness.destination);
  const liftGate = harness.gainNodes[3]!.gain;
  mixer.setTransportAnchor(0);
  mixer.setDriveLiftAvailableAt(0);

  harness.setCurrentTime(1);
  mixer.setDriveEnergy({ energy: 1, source: 'navigation' });
  expect(recordFor(harness, liftGate).events)
    .not.toContainEqual(expect.objectContaining({ kind: 'linear', value: 1 }));

  mixer.setDriveEnergy({ energy: 0.7, source: 'physical' });
  expect(recordFor(harness, liftGate).events).toContainEqual({
    kind: 'linear',
    value: 1,
    at: 4 * MUSIC_BEAT_SECONDS,
  });
});

it('cancels an unsustained entry and schedules hysteretic exit', () => {
  const harness = createFakeAudioHarness();
  const mixer = createAdaptiveMixer(harness.context, harness.destination);
  const liftGate = harness.gainNodes[3]!.gain;
  mixer.setTransportAnchor(0);
  mixer.setDriveLiftAvailableAt(0);
  harness.setCurrentTime(1);
  mixer.setDriveEnergy({ energy: 0.7, source: 'physical' });
  harness.setCurrentTime(1.2);
  mixer.setDriveEnergy({ energy: 0.5, source: 'physical' });
  expect(recordFor(harness, liftGate).events)
    .toContainEqual({ kind: 'hold', at: 1.2 });

  harness.setCurrentTime(2);
  mixer.setDriveEnergy({ energy: 0.7, source: 'physical' });
  harness.setCurrentTime(5);
  mixer.setDriveEnergy({ energy: 0.2, source: 'physical' });
  expect(recordFor(harness, liftGate).events)
    .toContainEqual(expect.objectContaining({ kind: 'linear', value: 0 }));
});

it('replays cached strong drive only after a late loop source is ready', () => {
  const harness = createFakeAudioHarness();
  const mixer = createAdaptiveMixer(harness.context, harness.destination);
  const liftGate = harness.gainNodes[3]!.gain;
  mixer.setDriveEnergy({ energy: 0.8, source: 'physical' });
  mixer.setTransportAnchor(0);
  expect(recordFor(harness, liftGate).events)
    .not.toContainEqual(expect.objectContaining({ kind: 'linear', value: 1 }));

  harness.setCurrentTime(5);
  mixer.setDriveLiftAvailableAt(5.08);
  const entryEnd = recordFor(harness, liftGate).events.findLast(
    (event) => event.kind === 'linear' && event.value === 1,
  );
  expect(entryEnd).toBeDefined();
  expect(entryEnd!.at).toBeGreaterThan(5.08 + 0.45);
  expect(entryEnd!.at / MUSIC_BEAT_SECONDS).toBeCloseTo(
    Math.round(entryEnd!.at / MUSIC_BEAT_SECONDS),
    8,
  );
});

Implement recordFor() in the test by finding the fake parameter record whose param is the supplied AudioParam. Add an idempotent disposal assertion.

  • Step 3: Run the focused transport/mixer tests and verify they fail

Run:

npm run test:unit -- tests/unit/soundtrack-transport.test.ts tests/unit/adaptive-mixer.test.ts

Expected: FAIL because adaptive constants, schedule helpers, mixer, and fake gain-node access do not exist.

  • Step 4: Pin all timing to exact samples

Add these constants to soundtrack-transport.ts:

export const MUSIC_BEAT_SECONDS = 60 / MUSIC_BPM;
export const MUSIC_BEAT_SAMPLES = Math.round(
  SOUNDTRACK_SAMPLE_RATE * MUSIC_BEAT_SECONDS,
);
export const MUSIC_BAR_SAMPLES = 133_953;
export const MAIN_DURATION_SAMPLES = 8_573_023;
export const MAIN_DURATION_SECONDS = MAIN_DURATION_SAMPLES / SOUNDTRACK_SAMPLE_RATE;
export const AFTERGLOW_TRANSITION_SAMPLES = MUSIC_BAR_SAMPLES;
export const AFTERGLOW_TRANSITION_SECONDS = AFTERGLOW_TRANSITION_SAMPLES / SOUNDTRACK_SAMPLE_RATE;
export const AFTERGLOW_LOOP_START_SAMPLE = AFTERGLOW_TRANSITION_SAMPLES;
export const AFTERGLOW_LOOP_SAMPLES = 1_071_628;
export const AFTERGLOW_LOOP_END_SAMPLE = 1_205_581;
export const AFTERGLOW_LOOP_START_SECONDS = AFTERGLOW_LOOP_START_SAMPLE / SOUNDTRACK_SAMPLE_RATE;
export const AFTERGLOW_LOOP_END_SECONDS = AFTERGLOW_LOOP_END_SAMPLE / SOUNDTRACK_SAMPLE_RATE;
export const DRIVE_LIFT_LOOP_SAMPLES = 535_814;
export const DRIVE_LIFT_LOOP_SECONDS = DRIVE_LIFT_LOOP_SAMPLES / SOUNDTRACK_SAMPLE_RATE;

Keep MUSIC_BAR_SECONDS = 4 * 60 / MUSIC_BPM for musical scheduling. Add sample-tolerant beat alignment and loop phase:

export function nextBeatTime(earliest: number, anchorTime: number): number {
  const elapsedBeats = (Math.max(anchorTime, earliest) - anchorTime) /
    MUSIC_BEAT_SECONDS;
  const oneSampleInBeats = 1 / SOUNDTRACK_SAMPLE_RATE / MUSIC_BEAT_SECONDS;
  return anchorTime + Math.ceil(elapsedBeats - oneSampleInBeats) * MUSIC_BEAT_SECONDS;
}

export function phaseAlignedLoopOffset(
  when: number,
  anchorTime: number,
  loopDuration: number,
): number {
  if (!Number.isFinite(loopDuration) || loopDuration <= 0) return 0;
  const elapsed = when - anchorTime;
  return ((elapsed % loopDuration) + loopDuration) % loopDuration;
}

export interface MainScheduleInput {
  readonly startTime: number;
  readonly offsetSeconds: number;
  readonly bufferDuration: number;
}

export interface MainSchedule {
  readonly sourceStartTime: number;
  readonly sourceOffsetSeconds: number;
  readonly musicalAnchorTime: number;
  readonly sourceEndTime: number;
  readonly afterglowStartTime: number;
}

export function resolveMainSchedule(input: MainScheduleInput): MainSchedule {
  const sourceOffsetSeconds = Math.min(
    Math.max(0, input.offsetSeconds),
    Math.max(0, input.bufferDuration - 0.05),
  );
  const sourceEndTime = input.startTime + input.bufferDuration - sourceOffsetSeconds;
  return {
    sourceStartTime: input.startTime,
    sourceOffsetSeconds,
    musicalAnchorTime: input.startTime - sourceOffsetSeconds,
    sourceEndTime,
    afterglowStartTime: sourceEndTime - AFTERGLOW_TRANSITION_SECONDS,
  };
}
  • Step 5: Construct the fixed-headroom mixer

Use these exact profiles in adaptive-mixer.ts:

const MOOD_RAMP_SECONDS = 1.4;
interface MixProfile {
  readonly bedGain: number;
  readonly highShelfDb: number;
  readonly textureSend: number;
  readonly liftCap: number;
}

const MIX_PROFILES = Object.freeze({
  open: { bedGain: 0.82, highShelfDb: 0, textureSend: 0.82, liftCap: 0.13 },
  shadow: { bedGain: 0.81, highShelfDb: -0.75, textureSend: 0.9, liftCap: 0.1 },
  lift: { bedGain: 0.82, highShelfDb: 0.65, textureSend: 0.9, liftCap: 0.17 },
  resolve: { bedGain: 0.81, highShelfDb: -0.15, textureSend: 0.74, liftCap: 0.11 },
} as const satisfies Readonly<Record<MusicMood, MixProfile>>);

Create gains in this exact order so the focused fake can identify the gate: bedInput, bedTrim, driveLiftInput, driveLiftGate, driveLiftCap, driveTextureInput, textureSend, sum. Route:

bedInput -> bedTrim -> highShelf -> sum -> destination
driveLiftInput -> driveLiftGate -> driveLiftCap -> sum
driveTextureInput -> textureSend -> sum

Set high shelf type to highshelf, frequency to 5200, Q to 0.55; initialize from open; initialize lift gate to zero. Do not create a compressor or stereo-width processor.

Apply mood changes through exactly four 1.4-second ramps:

const setChapterIntent = (intent: ChapterIntent): void => {
  if (disposed) return;
  const profile = MIX_PROFILES[intent.mood];
  ramp(bedTrim.gain, profile.bedGain, MOOD_RAMP_SECONDS);
  ramp(highShelf.gain, profile.highShelfDb, MOOD_RAMP_SECONDS);
  ramp(textureSend.gain, profile.textureSend, MOOD_RAMP_SECONDS);
  ramp(driveLiftCap.gain, profile.liftCap, MOOD_RAMP_SECONDS);
};
  • Step 6: Implement sustained, cancellable, beat-aligned drive lift

Use:

const LIFT_ENTRY_ENERGY = 0.62;
const LIFT_EXIT_ENERGY = 0.32;
const LIFT_ENTRY_DWELL_SECONDS = 0.45;
const LIFT_EXIT_DWELL_SECONDS = 0.8;
type LiftPhase = 'off' | 'entering' | 'on' | 'exiting';

let transportAnchor: number | null = null;
let driveLiftAvailableAt: number | null = null;
let latestDrive: DriveEnergySample = { energy: 0, source: 'idle' };
let liftPhase: LiftPhase = 'off';
let scheduledStart = 0;
let scheduledEnd = 0;

Reconcile elapsed scheduled states before each command:

const reconcileLift = (now: number): void => {
  if (liftPhase === 'entering' && now >= scheduledEnd) liftPhase = 'on';
  if (liftPhase === 'exiting' && now >= scheduledEnd) liftPhase = 'off';
};

Schedule entry from the first above-threshold sample, so remaining above threshold preserves the schedule while dropping below 0.62 before its start cancels it:

const scheduleEntry = (now: number): void => {
  if (
    transportAnchor === null ||
    driveLiftAvailableAt === null ||
    liftPhase === 'entering' ||
    liftPhase === 'on'
  ) return;
  scheduledStart = nextBeatTime(
    Math.max(now, driveLiftAvailableAt) + LIFT_ENTRY_DWELL_SECONDS,
    transportAnchor,
  );
  scheduledEnd = scheduledStart + MUSIC_BEAT_SECONDS;
  holdParameter(driveLiftGate.gain, now);
  driveLiftGate.gain.setValueAtTime(0, scheduledStart);
  driveLiftGate.gain.linearRampToValueAtTime(1, scheduledEnd);
  liftPhase = 'entering';
};

const cancelEntry = (now: number): void => {
  if (liftPhase !== 'entering' || now >= scheduledStart) return;
  holdParameter(driveLiftGate.gain, now);
  driveLiftGate.gain.linearRampToValueAtTime(0, now + 0.05);
  liftPhase = 'off';
};

For an audible layer, schedule release only after 800 ms below 0.32 and then fade over one beat:

const scheduleExit = (now: number): void => {
  if (
    transportAnchor === null ||
    driveLiftAvailableAt === null ||
    liftPhase === 'off' ||
    liftPhase === 'exiting'
  ) return;
  scheduledStart = nextBeatTime(
    now + LIFT_EXIT_DWELL_SECONDS,
    transportAnchor,
  );
  scheduledEnd = scheduledStart + MUSIC_BEAT_SECONDS;
  holdParameter(driveLiftGate.gain, now);
  driveLiftGate.gain.setValueAtTime(1, scheduledStart);
  driveLiftGate.gain.linearRampToValueAtTime(0, scheduledEnd);
  liftPhase = 'exiting';
};

const cancelExit = (now: number): void => {
  if (liftPhase !== 'exiting' || now >= scheduledStart) return;
  holdParameter(driveLiftGate.gain, now);
  driveLiftGate.gain.linearRampToValueAtTime(
    1,
    now + MUSIC_BEAT_SECONDS,
  );
  liftPhase = 'on';
};

Drive command routing always caches the latest sample, even while intro is running or the optional loop is still decoding:

const applyLatestDrive = (): void => {
  if (transportAnchor === null || driveLiftAvailableAt === null) return;
  const now = context.currentTime;
  reconcileLift(now);
  const physical = latestDrive.source === 'physical';
  if (physical && latestDrive.energy > LIFT_ENTRY_ENERGY) {
    if (liftPhase === 'exiting') {
      holdParameter(driveLiftGate.gain, now);
      driveLiftGate.gain.linearRampToValueAtTime(1, now + MUSIC_BEAT_SECONDS);
      liftPhase = 'on';
    } else {
      scheduleEntry(now);
    }
    return;
  }
  if (physical && latestDrive.energy >= LIFT_EXIT_ENERGY) {
    if (liftPhase === 'entering' && now < scheduledStart) cancelEntry(now);
    if (liftPhase === 'exiting') cancelExit(now);
    return;
  }
  if (liftPhase === 'entering' && now < scheduledStart) cancelEntry(now);
  if (!physical || latestDrive.energy < LIFT_EXIT_ENERGY) scheduleExit(now);
};

const setDriveEnergy = (sample: DriveEnergySample): void => {
  if (disposed) return;
  latestDrive = sample;
  applyLatestDrive();
};

Use one guarded mute/reset helper, and make both prerequisites replay the cached sample:

const resetLiftGate = (): void => {
  const now = context.currentTime;
  holdParameter(driveLiftGate.gain, now);
  driveLiftGate.gain.linearRampToValueAtTime(0, now + 0.05);
  liftPhase = 'off';
  scheduledStart = 0;
  scheduledEnd = 0;
};

const setTransportAnchor = (anchorTime: number | null): void => {
  if (disposed) return;
  transportAnchor = anchorTime;
  if (anchorTime === null) resetLiftGate();
  else applyLatestDrive();
};

const setDriveLiftAvailableAt = (startTime: number | null): void => {
  if (disposed) return;
  driveLiftAvailableAt = startTime;
  if (startTime === null) resetLiftGate();
  else applyLatestDrive();
};

A non-null anchor alone cannot make the layer audible; its decoded looping source must separately report its scheduled start time. dispose() calls resetLiftGate(), disconnects each owned mixer node once, and does not disconnect the supplied destination.

  • Step 7: Run the focused timing and mixer checks

Run:

npm run test:unit -- tests/unit/soundtrack-transport.test.ts tests/unit/adaptive-mixer.test.ts tests/unit/drive-texture.test.ts
npm run typecheck

Expected: all checks pass; navigation cannot arm lift; thresholds cannot chatter; four mood controls ramp independently; exact sample constants stay pinned.

  • Step 8: Commit transport and mixer behavior

Run:

git add src/audio/soundtrack-transport.ts src/audio/adaptive-mixer.ts tests/helpers/fake-audio-context.ts tests/unit/soundtrack-transport.test.ts tests/unit/adaptive-mixer.test.ts
git commit -m "feat: add adaptive soundtrack mixer"

Expected: one commit with pure transport math and the fixed adaptive graph, still not connected to live sources.

Task 9: Orchestrate the authored bed, cancellable intro, overlays, and texture

Files:

  • Modify: src/audio/soundtrack-engine.ts
  • Modify: tests/helpers/fake-audio-context.ts
  • Create: tests/unit/soundtrack-engine.test.ts
  • Modify: tests/browser/audio-interruption.spec.ts only if its existing semantic assertions require an adapter

Interfaces:

  • Consumes: four SoundtrackSources, AudioInitialState, AdaptiveMixer, DriveTexture, exact transport helpers, and the existing Opus-first/MP3-fallback loader.
  • Produces: one AudioGraph with terminal intro/main failures, independent optional-layer degradation, a cancellable Start exit, one immutable main anchor, a sample-addressed afterglow loop, a phase-aligned muted drive loop, preserved fault hooks, and exact-once cleanup.

Split browser construction from the injectable core:

export interface SoundtrackEngineDependencies {
  createContext(): AudioContext;
  loadBuffer(
    context: BaseAudioContext,
    variants: readonly AudioSourceVariant[],
    signal: AbortSignal,
  ): Promise<AudioBuffer>;
  createMixer: AdaptiveMixerFactory;
  createDriveTexture: DriveTextureFactory;
}

export function createSoundtrackEngine(
  sources: SoundtrackSources,
  initialState: AudioInitialState,
  dependencies: SoundtrackEngineDependencies,
): AudioGraph;

export function createBrowserSoundtrackEngine(
  sources: SoundtrackSources,
  initialState: AudioInitialState,
): AudioGraph;
  • Step 1: Write the focused engine test harness and failing tests

Use four unmistakable URLs and injected decoded buffers:

const sources: SoundtrackSources = {
  intro: [{ url: '/intro.opus', mediaType: 'audio/ogg' }],
  main: [{ url: '/main.opus', mediaType: 'audio/ogg' }],
  afterglow: [{ url: '/afterglow.opus', mediaType: 'audio/ogg' }],
  driveLift: [{ url: '/drive-lift.opus', mediaType: 'audio/ogg' }],
};

const initialState: AudioInitialState = {
  chapter: resolveChapterIntent('start'),
  drive: { energy: 0, source: 'idle' },
};

Have the loader return fake buffers with these exact durations:

const buffers = new Map([
  ['/intro.opus', harness.decodedBuffer(INTRO_RUNTIME_END_SECONDS)],
  ['/main.opus', harness.decodedBuffer(MAIN_DURATION_SECONDS)],
  ['/afterglow.opus', harness.decodedBuffer(AFTERGLOW_LOOP_END_SECONDS)],
  ['/drive-lift.opus', harness.decodedBuffer(DRIVE_LIFT_LOOP_SECONDS)],
]);

Have createEngineHarness() tag each returned fake buffer with the requesting variant URL and expose sourcesFor(url) by matching the source node's assigned buffer. This makes intro lead/loop/outro and main/optional ownership assertions deterministic without relying on source creation order.

Add these high-value cases:

it('cancels a Start departure before its audible bar boundary', async () => {
  const test = createEngineHarness(initialState);
  await test.graph.resume();
  test.graph.setChapterIntent(resolveChapterIntent('profile', 1));
  const pendingMain = test.sourcesFor('/main.opus').at(-1)!;
  const pendingOutro = test.sourcesFor('/intro.opus').at(-1)!;
  expect(pendingMain.starts).toHaveLength(1);
  expect(pendingOutro.starts).toHaveLength(1);

  test.graph.setChapterIntent(resolveChapterIntent('start', 2));
  expect(pendingMain.stops).toHaveLength(1);
  expect(pendingOutro.stops).toHaveLength(1);
  expect(test.mixer.setTransportAnchor).not.toHaveBeenCalledWith(expect.any(Number));
});

it('reuses one pending Start departure across rapid non-Start moods', async () => {
  const test = createEngineHarness(initialState);
  await test.graph.resume();
  test.graph.setChapterIntent(resolveChapterIntent('profile', 1));
  const mainCount = test.sourcesFor('/main.opus').length;
  const introCount = test.sourcesFor('/intro.opus').length;
  test.graph.setChapterIntent(resolveChapterIntent('experience', 2));
  test.graph.setChapterIntent(resolveChapterIntent('toolkit', 3));
  expect(test.sourcesFor('/main.opus')).toHaveLength(mainCount);
  expect(test.sourcesFor('/intro.opus')).toHaveLength(introCount);
  expect(test.mixer.setChapterIntent).toHaveBeenLastCalledWith(
    resolveChapterIntent('toolkit', 3),
  );
});

it('keeps the main anchor unchanged across every mood command', async () => {
  const test = createEngineHarness({
    chapter: resolveChapterIntent('experience'),
    drive: { energy: 0, source: 'idle' },
  });
  await test.graph.resume();
  const firstAnchor = test.mixer.setTransportAnchor.mock.calls.find(
    ([anchor]) => typeof anchor === 'number',
  )?.[0];
  test.graph.setChapterIntent(resolveChapterIntent('contact', 1));
  test.graph.setChapterIntent(resolveChapterIntent('start', 2));
  expect(test.mixer.setTransportAnchor.mock.calls.filter(
    ([anchor]) => typeof anchor === 'number',
  ).map(([anchor]) => anchor)).toEqual([firstAnchor]);
});

it('starts one afterglow transition and loops only its eight-bar idle region', async () => {
  const test = createEngineHarness({
    chapter: resolveChapterIntent('toolkit'),
    drive: { energy: 0, source: 'idle' },
  });
  await test.graph.resume();
  await test.flushOptionalLoads();
  const afterglow = test.sourceFor('/afterglow.opus');
  expect(afterglow.starts).toHaveLength(1);
  expect((afterglow.node as AudioBufferSourceNode).loop).toBe(true);
  expect((afterglow.node as AudioBufferSourceNode).loopStart)
    .toBe(AFTERGLOW_LOOP_START_SECONDS);
  expect((afterglow.node as AudioBufferSourceNode).loopEnd)
    .toBe(AFTERGLOW_LOOP_END_SECONDS);
});

it('contains optional failures and still resolves dominant playback', async () => {
  const test = createEngineHarness(initialState, {
    rejectAfterglow: true,
    rejectDriveLift: true,
    rejectDriveTexture: true,
  });
  await expect(test.graph.resume()).resolves.toBeUndefined();
  await test.flushOptionalLoads();
  expect(test.sourceFor('/intro.opus').starts).toHaveLength(1);
});

Add one direct-activation assertion for phrase-seven source offset, one phase-aligned late drive-source offset assertion, and one close-twice assertion proving each owned source/node is stopped/disconnected once. The harness must expose rejected optional loads as handled promises so the test runner reports no unhandled rejection.

  • Step 2: Run the focused engine test and verify it fails

Run:

npm run test:unit -- tests/unit/soundtrack-engine.test.ts

Expected: FAIL because the injectable engine, optional layer scheduling, and cancellable transition do not exist.

  • Step 3: Build the one-context graph and source ownership helpers

At construction, create exactly one context, output gain, mixer, and optional drive texture:

const context = dependencies.createContext();
const outputGain = context.createGain();
outputGain.gain.value = 0;
const mixer = dependencies.createMixer(context, outputGain);
let driveTexture: DriveTexture | null = null;
try {
  driveTexture = dependencies.createDriveTexture(
    context,
    mixer.driveTextureInput,
  );
} catch {
  driveTexture = null;
}

Track every scheduled source through one idempotent ownership record. Scheduled future stops remain connected until ended; cancellation and close release immediately. All paths share the same guarded cleanup, so a later ended event cannot disconnect twice:

interface OwnedSource {
  readonly node: AudioBufferSourceNode;
  stopAt(when: number): void;
  cancel(): void;
}

interface SourceHandle extends OwnedSource {
  readonly gain: GainNode;
}

const ownedSources = new Set<OwnedSource>();
const ownSource = (
  node: AudioBufferSourceNode,
  releaseDependents: () => void,
): OwnedSource => {
  let released = false;
  let stopCalled = false;
  let owned: OwnedSource;
  const release = (): void => {
    if (released) return;
    released = true;
    ownedSources.delete(owned);
    node.disconnect();
    releaseDependents();
  };
  const stopOnce = (when?: number): void => {
    if (stopCalled) return;
    stopCalled = true;
    try {
      if (when === undefined) node.stop();
      else node.stop(when);
    } catch {
      // An already-ended source is released by the same idempotent path.
    }
  };
  owned = {
    node,
    stopAt(when) { stopOnce(when); },
    cancel() {
      stopOnce();
      release();
    },
  };
  ownedSources.add(owned);
  node.addEventListener('ended', release, { once: true });
  return owned;
};

const makeSource = (
  buffer: AudioBuffer,
  destination: AudioNode,
): SourceHandle => {
  const node = context.createBufferSource();
  const gain = context.createGain();
  node.buffer = buffer;
  node.connect(gain);
  gain.connect(destination);
  const owned = ownSource(node, () => gain.disconnect());
  return { ...owned, gain };
};

const makeMarker = (
  when: number,
  onEnded: () => void,
): OwnedSource => {
  const node = context.createBufferSource();
  node.buffer = context.createBuffer(1, 1, context.sampleRate);
  const marker = ownSource(node, () => {});
  node.addEventListener('ended', onEnded, { once: true });
  node.start(when);
  return marker;
};

let introSourceNodes: SourceHandle[] = [];
let driveLiftSource: SourceHandle | null = null;
let afterglowSource: SourceHandle | null = null;
let mainSource: SourceHandle | null = null;
let loadedMainBuffer: AudioBuffer | null = null;
let mainSchedule: MainSchedule | null = null;

Use one abort controller for graph-owned requests. Dominant intro/main promises may reject resume(); optional afterglow/drive-lift promises must catch and resolve to null:

const loadOptional = (
  variants: readonly AudioSourceVariant[],
): Promise<AudioBuffer | null> => dependencies
  .loadBuffer(context, variants, loadController.signal)
  .catch(() => null);
  • Step 4: Start intro or direct main without delaying on optional assets

Keep the existing sample-addressed intro lead and loop. Start drive-lift loading only after context.resume() succeeds, without awaiting it:

let driveLiftBufferPromise: Promise<AudioBuffer | null> | null = null;
let afterglowBufferPromise: Promise<AudioBuffer | null> | null = null;
let optionalScheduleAttached = false;

driveLiftBufferPromise ??= loadOptional(sources.driveLift);
mainBufferPromise ??= dependencies.loadBuffer(
  context,
  sources.main,
  loadController.signal,
);

For direct activation, resolve the existing mapped phrase and create one immutable schedule:

const when = context.currentTime + SOURCE_LEAD_SECONDS;
const schedule = resolveMainSchedule({
  startTime: when,
  offsetSeconds: mainPhraseOffsetSeconds(latestChapter.entryPhrase),
  bufferDuration: mainBuffer.duration,
});
const main = makeSource(mainBuffer, mixer.bedInput);
main.gain.gain.setValueAtTime(0, when);
main.gain.gain.linearRampToValueAtTime(1, when + START_FADE_SECONDS);
main.node.start(when, schedule.sourceOffsetSeconds);
mainCommitted = true;
mainSource = main;
mainSchedule = schedule;
mixer.setTransportAnchor(schedule.musicalAnchorTime);
beginOptionalLayers(schedule);

Direct activation may publish its anchor immediately because the 450 ms drive-lift dwell is longer than SOURCE_LEAD_SECONDS. The intro path uses the identity-guarded mainStartMarker in Step 5, preventing drive lift from becoming audible during intro-idle or intro-out.

  • Step 5: Replace scheduled commitment with a cancellable transition record

Use:

interface PendingIntroExit {
  readonly requestedGeneration: number;
  readonly transitionTime: number;
  readonly outro: SourceHandle;
  readonly main: SourceHandle;
  readonly commitMarker: OwnedSource;
  readonly mainStartMarker: OwnedSource;
  readonly schedule: MainSchedule;
}

let pendingIntroExit: PendingIntroExit | null = null;

Schedule outro, main, gain automation, and two unconnected one-sample markers, but do not stop active intro sources and do not set mainCommitted yet:

const scheduleIntroExit = (
  introBuffer: AudioBuffer,
  mainBuffer: AudioBuffer,
  requestedGeneration: number,
): void => {
  if (closed || mainCommitted || pendingIntroExit !== null || latestChapter.atStart) return;
const transitionTime = nextSafeBarTime(
  Math.max(
    context.currentTime,
    introAnchorTime + INTRO_IN_END_SECONDS - EXIT_MINIMUM_LEAD_SECONDS,
  ),
  introAnchorTime,
  EXIT_MINIMUM_LEAD_SECONDS,
);
const outro = makeSource(introBuffer, mixer.bedInput);
outro.gain.gain.setValueAtTime(0, transitionTime);
outro.gain.gain.linearRampToValueAtTime(
  1,
  transitionTime + EXIT_CROSSFADE_SECONDS,
);
outro.node.start(
  transitionTime,
  INTRO_OUT_START_SECONDS,
  INTRO_RUNTIME_END_SECONDS - INTRO_OUT_START_SECONDS,
);

const mainTime = transitionTime + INTRO_OUT_END_SECONDS - INTRO_OUT_START_SECONDS;
const schedule = resolveMainSchedule({
  startTime: mainTime,
  offsetSeconds: 0,
  bufferDuration: mainBuffer.duration,
});
const main = makeSource(mainBuffer, mixer.bedInput);
main.gain.gain.setValueAtTime(0, mainTime);
main.gain.gain.linearRampToValueAtTime(1, mainTime + EXIT_CROSSFADE_SECONDS);
main.node.start(mainTime, 0);

Create the pending record before attaching its two identity-guarded marker callbacks. A later non-Start mood/generation reuses this same departure; only returning to Start or graph cancellation invalidates it:

let pending: PendingIntroExit;
const commitMarker = makeMarker(transitionTime, () => {
  if (closed || pendingIntroExit !== pending) return;
  mainCommitted = true;
  pendingIntroExit = null;
  mainSource = main;
  mainSchedule = schedule;
  for (const source of introSourceNodes) {
    source.stopAt(transitionTime + EXIT_CROSSFADE_SECONDS);
  }
  loadedIntroBuffer = null;
  beginOptionalLayers(schedule);
});
const mainStartMarker = makeMarker(mainTime, () => {
  if (mainSchedule !== schedule || closed) return;
  mixer.setTransportAnchor(schedule.musicalAnchorTime);
});
pending = {
  requestedGeneration,
  transitionTime,
  outro,
  main,
  commitMarker,
  mainStartMarker,
  schedule,
};
pendingIntroExit = pending;
};

Both callbacks execute after pending is assigned. requestIntroExit(requestedGeneration) returns immediately when pendingIntroExit !== null, so a rapid sequence such as Profile -> Experience -> Toolkit updates only the mix intent and never duplicates or stales the already scheduled departure. The main-start marker sets the mixer anchor only after the commit marker installs this exact schedule. A stale or stopped marker therefore does nothing.

Cancel only while context.currentTime < pendingIntroExit.transitionTime:

const cancelPendingIntroExit = (): void => {
  const pending = pendingIntroExit;
  if (pending === null || context.currentTime >= pending.transitionTime) return;
  pendingIntroExit = null;
  pending.outro.cancel();
  pending.main.cancel();
  pending.commitMarker.cancel();
  pending.mainStartMarker.cancel();
  if (introProgramGain !== null) {
    holdParameter(introProgramGain.gain, context.currentTime);
    introProgramGain.gain.linearRampToValueAtTime(
      1,
      context.currentTime + 0.05,
    );
  }
};

let exitLoadRequest: Promise<void> | null = null;
const requestIntroExit = (requestedGeneration: number): void => {
  if (closed || !introStarted || mainCommitted || pendingIntroExit !== null || exitLoadRequest !== null) return;
  const introBuffer = loadedIntroBuffer;
  const mainPromise = mainBufferPromise;
  if (introBuffer === null || mainPromise === null) return;
  exitLoadRequest = mainPromise.then((mainBuffer) => {
    if (!closed && !latestChapter.atStart && !mainCommitted) {
      scheduleIntroExit(introBuffer, mainBuffer, requestedGeneration);
    }
  }).catch(() => undefined).finally(() => {
    exitLoadRequest = null;
  });
};

setChapterIntent() always updates mixer mood. Before main commitment, non-Start intent requests one transition for its generation; Start intent cancels it. After commitment, Start changes only the mood to open and never touches sources or anchor.

  • Step 6: Schedule phase-aligned drive lift and the one-shot afterglow transition

When the optional drive buffer resolves after main commitment, start it muted through the mixer's gate:

const startDriveLift = (
  buffer: AudioBuffer,
  schedule: MainSchedule,
): void => {
  if (closed || driveLiftSource !== null) return;
  const when = Math.max(
    schedule.sourceStartTime,
    context.currentTime + SOURCE_LEAD_SECONDS,
  );
  const source = makeSource(buffer, mixer.driveLiftInput);
  source.node.loop = true;
  source.node.loopStart = 0;
  source.node.loopEnd = DRIVE_LIFT_LOOP_SECONDS;
  source.node.start(
    when,
    phaseAlignedLoopOffset(
      when,
      schedule.musicalAnchorTime,
      DRIVE_LIFT_LOOP_SECONDS,
    ),
  );
  driveLiftSource = source;
  mixer.setDriveLiftAvailableAt(when);
};

Begin afterglow loading only after main playback is established. If it resolves before main ends, start one looping source whose initial pass includes the transition but whose loop excludes it:

const startAfterglow = (
  buffer: AudioBuffer,
  schedule: MainSchedule,
): void => {
  if (closed || afterglowSource !== null || context.currentTime >= schedule.sourceEndTime) return;
  const when = Math.max(
    schedule.afterglowStartTime,
    context.currentTime + SOURCE_LEAD_SECONDS,
  );
  if (when >= schedule.sourceEndTime) return;
  const offset = Math.max(0, when - schedule.afterglowStartTime);
  if (offset >= AFTERGLOW_LOOP_END_SECONDS) return;
  const source = makeSource(buffer, mixer.bedInput);
  source.node.loop = true;
  source.node.loopStart = AFTERGLOW_LOOP_START_SECONDS;
  source.node.loopEnd = AFTERGLOW_LOOP_END_SECONDS;
  const progress = Math.min(1, offset / AFTERGLOW_TRANSITION_SECONDS);
  source.gain.gain.setValueAtTime(progress, when);
  source.gain.gain.linearRampToValueAtTime(1, schedule.sourceEndTime);
  source.node.start(when, offset);
  afterglowSource = source;
};

const beginOptionalLayers = (schedule: MainSchedule): void => {
  if (closed || optionalScheduleAttached) return;
  optionalScheduleAttached = true;
  const drivePromise = driveLiftBufferPromise ?? loadOptional(sources.driveLift);
  driveLiftBufferPromise = drivePromise;
  void drivePromise.then((buffer) => {
    driveLiftBufferPromise = null;
    if (buffer !== null) startDriveLift(buffer, schedule);
  });
  const afterglowPromise = loadOptional(sources.afterglow);
  afterglowBufferPromise = afterglowPromise;
  void afterglowPromise.then((buffer) => {
    afterglowBufferPromise = null;
    if (buffer !== null) startAfterglow(buffer, schedule);
  });
};

The main source ended listener clears loadedMainBuffer, mainBufferPromise, and its handle. The intro references clear after transition. The afterglow continues indefinitely and the transition bar never repeats.

  • Step 7: Route independent commands and restore authenticated fault behavior

Implement:

setChapterIntent(intent) {
  if (closed) return;
  latestChapter = intent;
  mixer.setChapterIntent(intent);
  if (!mainCommitted && introStarted) {
    if (intent.atStart) cancelPendingIntroExit();
    else requestIntroExit(intent.generation);
  }
},
setDriveEnergy(sample) {
  if (closed) return;
  latestDrive = sample;
  driveTexture?.setEnergy(sample);
  mixer.setDriveEnergy(sample);
},

Restore the old F10-F13 wrapper around createSoundtrackEngine: F10 throws before constructing a context; F11 rejects only a post-interruption resume; F12 produces InvalidStateError on that resume; F13 rejects suspend. Keep recordTestFaultEvent() calls and all existing browser-visible behavior unchanged.

  • Step 8: Make cancellation and close idempotent

cancelPending() aborts fetches and cancels only pre-boundary intro work. close() must guard closed, abort requests, cancel pending markers, dispose texture and mixer, stop every remaining source, remove the context listener, disconnect output, and close the context once:

async close() {
  if (closed) return;
  closed = true;
  loadController.abort();
  cancelPendingIntroExit();
  mixer.setDriveLiftAvailableAt(null);
  driveTexture?.dispose();
  mixer.dispose();
  for (const source of [...ownedSources]) source.cancel();
  ownedSources.clear();
  context.removeEventListener('statechange', onStateChange);
  stateListeners.clear();
  outputGain.disconnect();
  if (context.state !== 'closed') await context.close();
},

Ensure cancelPendingIntroExit() can run during close even after closed = true; it must not depend on the open flag.

  • Step 9: Run the focused graph and lifecycle checks

Run:

npm run test:unit -- tests/unit/soundtrack-engine.test.ts tests/unit/adaptive-mixer.test.ts tests/unit/drive-texture.test.ts tests/unit/soundtrack-transport.test.ts tests/unit/audio-lifecycle.test.ts
npm run typecheck

Expected: all checks pass; optional failures are contained; main anchor never changes; Start exit is cancellable until its audible boundary; afterglow and drive lift share the clock.

  • Step 10: Commit the complete adaptive graph

Run:

git add src/audio/soundtrack-engine.ts tests/helpers/fake-audio-context.ts tests/unit/soundtrack-engine.test.ts
git add tests/browser/audio-interruption.spec.ts
git commit -m "feat: add afterglow and synchronized drive lift"

If tests/browser/audio-interruption.spec.ts required no source edit, omit it from git add. Expected: one graph commit; large WAVs and external production files remain outside the repository.

Task 10: Calibrate once, build the trusted preview, and run the final listening gate

Files:

  • Modify only if listening requires calibration: src/audio/adaptive-mixer.ts
  • Modify only if listening requires calibration: src/audio/drive-texture.ts
  • Verify: all files changed in Tasks 3-9
  • External output: /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/maximum-combined-mix.wav

Interfaces:

  • Consumes: the complete adaptive implementation and approved short masters.

  • Produces: one technically screened maximum-state render, one authenticated browser regression invocation, one trusted compiled preview, one abuse/listening pass, and explicit user acceptance or actionable listening feedback.

  • Step 1: Render a conservative maximum-state headroom check

Run this conservative offline ceiling using the exact maximum additive gains shipped in Task 8. It deliberately applies the maximum +0.65 dB shelf as full-band gain and overlays the afterglow transition against the louder lift phrase, so it overstates rather than understates the browser graph:

ffmpeg -v error -y \
  -i /home/luca/Projects/nightdrive-music/generated/production/hybrid-v1/main-score-64bars-master-candidate-v4.wav \
  -i /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-drive-lift.wav \
  -i /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/nightdrive-afterglow-runtime.wav \
  -f lavfi -i 'sine=frequency=62:sample_rate=48000:duration=20' \
  -f lavfi -i 'sine=frequency=124:sample_rate=48000:duration=20' \
  -f lavfi -i 'anoisesrc=color=pink:amplitude=0.1:sample_rate=48000:duration=20' \
  -filter_complex '[0:a]atrim=start_sample=6429767:end_sample=7389767,asetpts=PTS-STARTPTS,volume=0.883718[bed];[1:a]aloop=loop=-1:size=535814,atrim=end_sample=960000,asetpts=PTS-STARTPTS,volume=0.17[lift];[2:a]atrim=start_sample=0:end_sample=133953,asetpts=PTS-STARTPTS,afade=t=in:ss=0:ns=133953:curve=tri,volume=0.883718[afterglow];[3:a]volume=0.012[engine0];[4:a]volume=0.0042[engine1];[5:a]highpass=f=180,lowpass=f=3400,volume=0.024[road];[bed][lift][afterglow][engine0][engine1][road]amix=inputs=6:duration=shortest:normalize=0[out]' \
  -map '[out]' -ar 48000 -ac 2 -c:a pcm_f32le \
  /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/maximum-combined-mix.wav
/home/luca/Projects/nightdrive-music/ACE-Step-1.5/.venv/bin/python /home/luca/Projects/nightdrive-music/skills/nightdrive-music/scripts/analyze_audio.py /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/maximum-combined-mix.wav
ffmpeg -hide_banner -nostats -i /home/luca/Projects/nightdrive-music/generated/production/adaptive-v1/verification/maximum-combined-mix.wav -filter_complex 'ebur128=peak=true' -f null -

Expected: finite 48 kHz stereo output, sample peak below 1.0, and true peak below 0 dBFS despite the conservative overstatement. This is a technical ceiling check, not evidence that the mix sounds good; the actual browser graph is also checked audibly at volume 1 in Step 5. If it clips, reduce only liftCap, MAX_ENGINE_GAIN, or MAX_ROAD_GAIN; do not compress, limit, or reduce intro/main encoding quality.

  • Step 2: Run the final focused static checks once

Run:

git diff --check
if rg -n "setJourneyState|AudioJourneyState|pendingAudioDestination|createMusicSequencer" src tests; then
  echo 'Removed audio APIs remain in runtime or tests.' >&2
  exit 1
fi
npm run test:unit -- tests/unit/audio-intent-coordinator.test.ts tests/unit/use-audio-intent.test.ts tests/unit/navigation-controller.test.ts tests/unit/section-controller.test.ts tests/unit/audio-lifecycle.test.ts tests/unit/drive-texture.test.ts tests/unit/adaptive-mixer.test.ts tests/unit/soundtrack-transport.test.ts tests/unit/soundtrack-engine.test.ts tests/unit/content-schemas.test.ts tests/integration/public-content.test.ts tests/integration/materialize-empty-asset-set.test.mjs tests/integration/verify-artifacts.test.ts tests/performance/budgets.test.ts
npm run typecheck
npm run verify:approved-assets

Expected: git diff --check prints nothing; the search prints no runtime references to removed combined/procedural APIs; all focused tests, typecheck, and exact asset verification pass.

  • Step 3: Run the one authenticated browser and compiled-build gate

Run exactly one Playwright invocation:

npx playwright test tests/browser/audio-controls.spec.ts tests/browser/audio-interruption.spec.ts --project=desktop-chromium --workers=1

Expected: gesture creation, repeated activation, mute/volume, visibility, interruption, F10-F13 containment, and resource cleanup pass. The existing authenticated browser harness runs npm run build:preview for its test build and restores a clean trusted dist with the same command during teardown; do not run an additional standalone full build.

  • Step 4: Serve the compiled output through the established trusted path

Run from the worktree and keep the process active:

./node_modules/.bin/vite preview --host 127.0.0.1 --port 4173 --strictPort

Expected: http://127.0.0.1:4173/ renders the complete portfolio. Do not use npm run dev; it does not serve the prerendered production-input snapshot used by this site.

  • Step 5: Perform one browser abuse pass

Use this single ordered pass while listening at a normal level and again briefly at volume 1:

  1. Activate on Start and let intro-idle cross at least two loop seams; confirm no click, restart, volume hole, or drift.
  2. Move a few pixels around the Start/Profile boundary, reverse, and pause on each side without going 6vh past it; confirm no chapter shade and no intro departure.
  3. Move more than 6vh past the line and remain there for 400 ms; confirm one subtle 1.4-second open shade and one bar-safe intro departure.
  4. Use the navigator rapidly for Start, Contact, and Toolkit; confirm the master stays continuous and only the latest settled Toolkit intent shades to resolve.
  5. Interrupt a navigator trip with wheel or touch input; confirm the stale destination never becomes musical state.
  6. Apply sustained strong physical scrolling for more than 450 ms; confirm immediate restrained engine/air response and drive lift only on the next beat. Release and confirm a smooth delayed one-beat exit.
  7. Return to Start after main commitment; confirm the main continues and only the mood returns to open.
  8. Resize the viewport and change content height while audio is active; confirm no mood change occurs from layout alone.
  9. Mute, change volume, hide/show the page, and resume an interruption; confirm one live context and preserved transport.
  10. Reload directly at #contact, activate there, wait through the remaining final phrase, then listen through three afterglow-idle repetitions; confirm the transition bar plays once and both loop seams disappear perceptually.

Record any audible issue with the section, action, and wall-clock time. Do not respond by adding a new transition system or seeking the master.

  • Step 6: Stop for the final in-site human listening checkpoint

Tell the user the compiled preview is ready at http://127.0.0.1:4173/ and summarize only:

  • the approved afterglow and drive-lift seeds;
  • whether the maximum-state render retained headroom;
  • that tiny-scroll, rapid-jump, sustained-acceleration, return-to-Start, resize, lifecycle, and three-repeat afterglow scenarios passed;
  • any remaining audible caveat.

Ask the user to approve the in-site result. Do not declare the soundtrack complete until approval. If the user reports a specific audible defect, adjust only the implicated profile/send/envelope or short asset, rerun that task's focused test and the affected listening excerpt, then return to this checkpoint.

  • Step 7: Commit only a calibration change, if one was required

When Step 1 or the listening pass changed constants, run:

git add src/audio/adaptive-mixer.ts src/audio/drive-texture.ts tests/unit/adaptive-mixer.test.ts tests/unit/drive-texture.test.ts
git commit -m "fix: calibrate adaptive soundtrack balance"

If no calibration source changed, create no empty commit. Expected final status: no uncommitted implementation or asset changes, and the user has explicitly approved the compiled preview.

Acceptance Trace

  • Tiny-scroll stability: Tasks 4-6, verified in Task 10 steps 2 and 5.
  • Rapid navigator supersession: Tasks 4-6, verified in Task 10 steps 2 and 5.
  • Arbitrary Start dwell and cancellable departure: Task 9, verified in Task 10 step 5.
  • Continuous main and return-to-Start behavior: Tasks 8-9, verified in Task 10 step 5.
  • Infinite afterglow with a non-repeating transition: Tasks 2, 8, and 9, verified across three repetitions in Tasks 2 and 10.
  • Four subtle moods: Tasks 5, 6, and 8, finalized by the Task 10 listening gate.
  • Immediate drive texture and sustained drive lift: Tasks 5, 7, 8, and 9, finalized by the Task 10 listening gate.
  • Responsive content/layout safety: Tasks 4-6, verified in Task 10 step 5.
  • Gesture/lifecycle/failure/teardown safety: Tasks 6 and 9, verified by Task 10 step 3.
  • Hash closure and 16 MiB delivery cap: Task 3, verified by Task 10 steps 2 and 3.