Player Stack Contract (AudioPlayer / PlayerState / AudioEngine)
1. Overview
This document defines the intended contract between:
AudioPlayer.svelte– UI container and interaction surfacePlayerState.svelte.js– state machine for playback state and queueuseAudioWiring.svelte.js– bridge betweenPlayerStateand the HTML5<audio>element + MediaSessionlib/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.jsbridge was never constructed at runtime and has been deleted.useAudioWiring.svelte.jsis the bridge: it binds the<audio>events intoPlayerStateand owns MediaSession (metadata on every track change; play, pause, previoustrack, nexttrack and seekto handlers). Where sections 2.3–5 below sayAudioEngine.svelte.js, readuseAudioWiring.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 viabind:this. - Translates user interactions (clicks, keyboard shortcuts, slider changes) into calls on
PlayerStateand 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
PlayerStateowns 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 intoPlayerState. - 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.
- Called once after mount with the
- Event → state wiring:
timeupdate→playerState.updateTime(audio.currentTime)loadedmetadata→playerState.setDuration(audio.duration)canplaythrough→ ifplayerState.isPlaying, attemptaudio.play(); on failure, log andsetPlaying(false).ended→playerState.nextTrack().error→ log error with track id andsetPlaying(false).play/pause→ mirror intoplayerState.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)setsaudio.srcto/audio/${track.id}and loads+plays based onplayerState.isPlaying.
- Control methods (used by UI):
play(),pause(),seek(time),setVolume(volume),setMuted(muted).
Ownership Rules
AudioEngineowns how intents fromPlayerStateare 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
AudioContextgraph (gain, compressor, 3‑band EQ). - Supports spectrum and waveform analysis for visualizers.
- Implements master volume shaping and crossfade behavior.
- Creates and manages a
Responsibilities
initialize(audioElement)– connects the<audio>element into the Web Audio graph exactly once.- EQ:
setEQ(band, value)andapplyEQPreset(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()andgetWaveformData()for render loops.
- Lifecycle:
resume()to handle user gesture requirements.destroy()to clean up and close theAudioContext.
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
- User acts in the UI (click play, next, seek, adjust volume/EQ, toggle crossfade).
AudioPlayer.sveltetranslates the gesture into:- A call on
PlayerState(playTrack,togglePlay,nextTrack,seek,setVolume, etc.). - Optionally a call on the Web Audio engine (
setVolume,setEQ,applyEQPreset).
- A call on
- State changes in
PlayerState(e.g.,currentTrack,isPlaying,volume). - AudioEngine.svelte.js reacts to
PlayerStatechanges and drives the<audio>element:- Loads the correct
srcwhencurrentTrackchanges. - Calls
audio.play()/audio.pause()whenisPlayingchanges. - Keeps duration and time in sync via events.
- Loads the correct
- 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 legacyplayerStore.playTrack(...)until migration is complete.
- Expected effects:
PlayerState.currentTrackandqueueupdate.AudioEngine.loadTrack(track)is called by whatever wiring watchescurrentTrack.- The
<audio>element gets itssrcupdated and is loaded. - If the state says we should be playing,
AudioEnginestarts playback.
3.3 Play/Pause Contract
- UI play/pause button calls one place for intent:
PlayerState.togglePlay()(preferred) or legacyplayerStore.togglePlay().
- AudioEngine responds to
isPlayingand 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/pauseevents from the element must always callplayerState.setPlaying(true/false)so state and DOM cannot diverge.
- If intent is “play”, it attempts
3.4 Next/Previous Contract
- UI controls call
PlayerState.nextTrack()/prevTrack(). PlayerStatedecides the nextcurrentTrackbased on:repeatMode(none,all,one).isShuffledflag.queueorder.
- AudioEngine simply reacts to
currentTrackchanging and loads/plays the chosen track.
3.5 Volume & Mute Contract
- “User volume” is owned in
PlayerState.volumeandPlayerState.isMuted. - The UI forwards changes to both
PlayerStateand the engines:- Call
playerState.setVolume(newVolume). - Call Web Audio
setVolume(...)/handleCrossfade(...)as needed for DSP.
- Call
- AudioEngine.svelte.js honours the canonical
PlayerStatevolume when settingaudio.volume.
4. Lifecycle & Effects
4.1 Construction
+layout.svelte(or a root feature module) constructs onePlayerStateinstance.AudioPlayer.sveltereceives or accesses thatPlayerStateinstance and creates:- A Web Audio engine for DSP (
new AudioEngine()fromlib/audio/engine.js). - A bridge for the
<audio>element (AudioEngine.svelte.js) once integrated.
- A Web Audio engine for DSP (
4.2 Mount & Init
On first mount of AudioPlayer in the browser:
<audio bind:this={audioEl}>becomes available.- Web Audio engine
initialize(audioEl)is called after a user gesture when necessary. - AudioEngine.svelte.js
init(audioEl)is called to bind DOM events and MediaSession. - Any
$effect.rootthat wiresPlayerState↔ AudioEngine ↔ Web Audio is kept in one small helper zone, to avoid scattered effects andeffect_orphannoise.
4.3 Teardown
- When
AudioPlayeris destroyed:- Web Audio engine
destroy()is called to close theAudioContextand stop visualization loops. - AudioEngine.svelte.js
destroy()is called to pause the element, clearsrc, and remove listeners. - Any
$effect.rootcleanup functions are executed exactly once.
- Web Audio engine
5. Current vs Target Implementation
5.1 Current Reality (as of Dec 2025)
PlayerState.svelte.jsandAudioEngine.svelte.jsexist and are exported from the player feature module.AudioPlayer.sveltestill uses a legacyplayerStorepluslib/audio/engine.jsfor Web Audio, and wires DOM events inside the component.- The root layout already instantiates a
PlayerState, butAudioPlayeris not yet fully migrated to use it.
5.2 Target Direction
Refactors for CHECKLIST Section 2 should:
- Adopt
PlayerStateas the single source of truth for playback state. - Move DOM event wiring and MediaSession integration into
AudioEngine.svelte.js, keepingAudioPlayeras a declarative view. - Retain Web Audio engine strictly as a DSP layer, driven by canonical state/volume from
PlayerState. - Centralize
$effect.rootusage around player wiring, ideally in a tiny helper or inside the player feature module, to eliminateeffect_orphanwarnings.
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).