Player Stack Contract (AudioPlayer / PlayerState / AudioEngine)

1. Overview

This document defines the intended contract between:

  • AudioPlayer.svelte – UI container and interaction surface
  • PlayerState.svelte.js – state machine for playback state and queue
  • useAudioWiring.svelte.js – bridge between PlayerState and the HTML5 <audio> element + MediaSession
  • lib/audio/engine.js – Web Audio API engine used by the UI for EQ, crossfade, and visualizations

Decision 2026-10-05 (D-322b, owner choice Q4a): the planned AudioEngine.svelte.js bridge was never constructed at runtime and has been deleted. useAudioWiring.svelte.js is the bridge: it binds the <audio> events into PlayerState and owns MediaSession (metadata on every track change; play, pause, previoustrack, nexttrack and seekto handlers). Where sections 2.3–5 below say AudioEngine.svelte.js, read useAudioWiring.svelte.js.

Goal: make the player stack easier to reason about, refactor, and test, while reducing effect_orphan-style issues and keeping SSR-safe runes usage.

2. Responsibilities

2.1 AudioPlayer.svelte (View + Orchestration Shell)

Role

  • Renders the visible player UI (controls, timeline, metadata, EQ panel, visualizers, queue view).
  • Owns the actual <audio> DOM element via bind:this.
  • Translates user interactions (clicks, keyboard shortcuts, slider changes) into calls on PlayerState and AudioEngine.

Must NOT

  • Contain long-running business logic or ad‑hoc playback state mutations.
  • Reach into the <audio> element for anything other than wiring through AudioEngine.
  • Duplicate state that is already owned by PlayerState.

May

  • Hold ephemeral UI-only flags such as isExpanded, panel visibility, or local hover/focus state.
  • Coordinate feature components (EQ controls, visualizers, queue view) by passing them the subset of state/engine they need.

2.2 PlayerState.svelte.js (State Machine)

Role

  • Single source of truth for logical playback state, independent of DOM:
    • isPlaying, currentTrack, queue, currentTime, duration
    • playback modes: isShuffled, repeatMode
    • volume/mute: volume, isMuted
  • Implements state transitions:
    • playTrack(track, queue)
    • togglePlay(), setPlaying(playing)
    • toggleShuffle(), toggleRepeat()
    • nextTrack(), prevTrack()
    • updateTime(time), setDuration(duration), seek(time)
    • setVolume(vol), toggleMute()
    • clear()
  • Exposes derived values (runes):
    • queueLength, currentIndex, hasNext, hasPrevious, progress.

Ownership Rules

  • PlayerState owns intent (what should be playing, where we are in the queue, whether we want to be playing or paused).
  • It does not talk to the DOM, the <audio> element, or Web Audio directly.

2.3 AudioEngine.svelte.js (HTML5 Audio + MediaSession Bridge)

Role

  • Wraps a single <audio> element and binds media events back into PlayerState.
  • Integrates with the MediaSession API so OS‑level controls use the same state.

Responsibilities

  • init(audioElement)
    • Called once after mount with the <audio> element.
    • Saves a reference and registers all event listeners.
  • Event → state wiring:
    • timeupdate → playerState.updateTime(audio.currentTime)
    • loadedmetadata → playerState.setDuration(audio.duration)
    • canplaythrough → if playerState.isPlaying, attempt audio.play(); on failure, log and setPlaying(false).
    • ended → playerState.nextTrack().
    • error → log error with track id and setPlaying(false).
    • play / pause → mirror into playerState.setPlaying(true/false).
  • MediaSession wiring:
    • play / pause → this.play() / this.pause().
    • previoustrack / nexttrack → playerState.prevTrack() / playerState.nextTrack().
    • seekto → this.seek(details.seekTime).
  • Track loading:
    • loadTrack(track) sets audio.src to /audio/${track.id} and loads+plays based on playerState.isPlaying.
  • Control methods (used by UI):
    • play(), pause(), seek(time), setVolume(volume), setMuted(muted).

Ownership Rules

  • AudioEngine owns how intents from PlayerState are realized on the <audio> element and MediaSession.
  • It must not make its own decisions about what track to play next – that comes from PlayerState.

2.4 lib/audio/engine.js (Web Audio Engine)

Role

  • Provides advanced audio processing and visualization on top of the HTML5 audio element:
    • Creates and manages a AudioContext graph (gain, compressor, 3‑band EQ).
    • Supports spectrum and waveform analysis for visualizers.
    • Implements master volume shaping and crossfade behavior.

Responsibilities

  • initialize(audioElement) – connects the <audio> element into the Web Audio graph exactly once.
  • EQ:
    • setEQ(band, value) and applyEQPreset(presetName) for 3‑band EQ.
    • getCurrentEQ() for inspection.
  • Volume & crossfade:
    • setVolume(value) for smooth master‑volume changes.
    • handleCrossfade(currentTime, duration, crossfadeDuration, masterVolume, isMuted) for end‑of‑track auto‑fade.
  • Visualization:
    • getFrequencyData() and getWaveformData() for render loops.
  • Lifecycle:
    • resume() to handle user gesture requirements.
    • destroy() to clean up and close the AudioContext.

Ownership Rules

  • This engine never decides tracks or queue; it only manipulates the sound of whatever the <audio> element is currently playing.
  • It is a pure enhancement layer that can be turned off without breaking basic playback.

3. Data & Control Flow

3.1 High-Level Flow

  1. User acts in the UI (click play, next, seek, adjust volume/EQ, toggle crossfade).
  2. AudioPlayer.svelte translates the gesture into:
    • A call on PlayerState (playTrack, togglePlay, nextTrack, seek, setVolume, etc.).
    • Optionally a call on the Web Audio engine (setVolume, setEQ, applyEQPreset).
  3. State changes in PlayerState (e.g., currentTrack, isPlaying, volume).
  4. AudioEngine.svelte.js reacts to PlayerState changes and drives the <audio> element:
    • Loads the correct src when currentTrack changes.
    • Calls audio.play() / audio.pause() when isPlaying changes.
    • Keeps duration and time in sync via events.
  5. Web Audio engine (lib/audio/engine.js) hooks into the same <audio> element and shapes audio + visualizations.

3.2 Track Change Contract

When the currently selected track should change:

  • The UI (track list, playlist page, etc.) must call:
    • PlayerState.playTrack(track, queue) or the legacy playerStore.playTrack(...) until migration is complete.
  • Expected effects:
    • PlayerState.currentTrack and queue update.
    • AudioEngine.loadTrack(track) is called by whatever wiring watches currentTrack.
    • The <audio> element gets its src updated and is loaded.
    • If the state says we should be playing, AudioEngine starts playback.

3.3 Play/Pause Contract

  • UI play/pause button calls one place for intent:
    • PlayerState.togglePlay() (preferred) or legacy playerStore.togglePlay().
  • AudioEngine responds to isPlaying and events:
    • If intent is “play”, it attempts audio.play() and may fall back / log on failure.
    • If intent is “pause”, it calls audio.pause().
    • play / pause events from the element must always call playerState.setPlaying(true/false) so state and DOM cannot diverge.

3.4 Next/Previous Contract

  • UI controls call PlayerState.nextTrack() / prevTrack().
  • PlayerState decides the next currentTrack based on:
    • repeatMode (none, all, one).
    • isShuffled flag.
    • queue order.
  • AudioEngine simply reacts to currentTrack changing and loads/plays the chosen track.

3.5 Volume & Mute Contract

  • “User volume” is owned in PlayerState.volume and PlayerState.isMuted.
  • The UI forwards changes to both PlayerState and the engines:
    • Call playerState.setVolume(newVolume).
    • Call Web Audio setVolume(...) / handleCrossfade(...) as needed for DSP.
  • AudioEngine.svelte.js honours the canonical PlayerState volume when setting audio.volume.

4. Lifecycle & Effects

4.1 Construction

  • +layout.svelte (or a root feature module) constructs one PlayerState instance.
  • AudioPlayer.svelte receives or accesses that PlayerState instance and creates:
    • A Web Audio engine for DSP (new AudioEngine() from lib/audio/engine.js).
    • A bridge for the <audio> element (AudioEngine.svelte.js) once integrated.

4.2 Mount & Init

On first mount of AudioPlayer in the browser:

  1. <audio bind:this={audioEl}> becomes available.
  2. Web Audio engine initialize(audioEl) is called after a user gesture when necessary.
  3. AudioEngine.svelte.js init(audioEl) is called to bind DOM events and MediaSession.
  4. Any $effect.root that wires PlayerState ↔ AudioEngine ↔ Web Audio is kept in one small helper zone, to avoid scattered effects and effect_orphan noise.

4.3 Teardown

  • When AudioPlayer is destroyed:
    • Web Audio engine destroy() is called to close the AudioContext and stop visualization loops.
    • AudioEngine.svelte.js destroy() is called to pause the element, clear src, and remove listeners.
    • Any $effect.root cleanup functions are executed exactly once.

5. Current vs Target Implementation

5.1 Current Reality (as of Dec 2025)

  • PlayerState.svelte.js and AudioEngine.svelte.js exist and are exported from the player feature module.
  • AudioPlayer.svelte still uses a legacy playerStore plus lib/audio/engine.js for Web Audio, and wires DOM events inside the component.
  • The root layout already instantiates a PlayerState, but AudioPlayer is not yet fully migrated to use it.

5.2 Target Direction

Refactors for CHECKLIST Section 2 should:

  1. Adopt PlayerState as the single source of truth for playback state.
  2. Move DOM event wiring and MediaSession integration into AudioEngine.svelte.js, keeping AudioPlayer as a declarative view.
  3. Retain Web Audio engine strictly as a DSP layer, driven by canonical state/volume from PlayerState.
  4. Centralize $effect.root usage around player wiring, ideally in a tiny helper or inside the player feature module, to eliminate effect_orphan warnings.

Once this contract is implemented, AudioPlayer.svelte becomes easier to split (Task 2.2), orchestration can move fully into PlayerState / AudioEngine (Task 2.3), and $effect.root usage can be simplified and audited (Task 2.4).