gapless 4.4.0 → 4.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Queue.d.ts +72 -0
- package/dist/Queue.d.ts.map +1 -0
- package/dist/Track.d.ts +150 -0
- package/dist/Track.d.ts.map +1 -0
- package/dist/index.d.ts +4 -318
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/machines/fetchDecode.machine.d.ts +42 -0
- package/dist/machines/fetchDecode.machine.d.ts.map +1 -0
- package/dist/machines/queue.machine.d.ts +165 -0
- package/dist/machines/queue.machine.d.ts.map +1 -0
- package/dist/machines/track.machine.d.ts +291 -0
- package/dist/machines/track.machine.d.ts.map +1 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/audioContext.d.ts +25 -0
- package/dist/utils/audioContext.d.ts.map +1 -0
- package/dist/utils/mediaSession.d.ts +17 -0
- package/dist/utils/mediaSession.d.ts.map +1 -0
- package/dist/utils/throttle.d.ts +3 -0
- package/dist/utils/throttle.d.ts.map +1 -0
- package/package.json +10 -10
- package/src/Queue.ts +6 -5
- package/src/Track.ts +5 -1
- package/src/machines/queue.machine.ts +10 -6
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Queue.ts","../src/utils/audioContext.ts","../src/utils/mediaSession.ts","../src/machines/queue.machine.ts","../src/Track.ts","../src/machines/track.machine.ts","../src/machines/fetchDecode.machine.ts","../src/utils/throttle.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Queue — public API class; orchestrates tracks via QueueMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor } from 'xstate';\nimport { getAudioContext, resumeAudioContext as _resumeAudioContext } from './utils/audioContext';\nimport {\n setupMediaSession,\n updateMediaSessionMetadata,\n updateMediaSessionPlaybackState,\n updateMediaSessionPositionState,\n} from './utils/mediaSession';\nimport { createQueueMachine } from './machines/queue.machine';\nimport { Track } from './Track';\nimport type { TrackQueueRef } from './Track';\nimport { throttle } from './utils/throttle';\nimport type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata, PlaybackMethod } from './types';\n\nconst MAX_SCHEDULE_LOOKAHEAD = 5;\n\n/**\n * Lazy singleton Blob URL for a 10-second silent WAV.\n * Chrome requires a media element with >= 5 s intrinsic duration to treat it\n * as a \"controllable\" media session. Generated once at first use; 8 kHz mono\n * 8-bit keeps it ~80 KB.\n */\nlet _silentWavUrl: string | null = null;\nfunction getSilentWavUrl(): string {\n if (_silentWavUrl) return _silentWavUrl;\n const sampleRate = 8000;\n const numSamples = sampleRate * 10;\n const fileSize = 44 + numSamples;\n const buffer = new ArrayBuffer(fileSize);\n const view = new DataView(buffer);\n\n const writeStr = (off: number, s: string) => {\n for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));\n };\n writeStr(0, 'RIFF');\n view.setUint32(4, fileSize - 8, true);\n writeStr(8, 'WAVE');\n writeStr(12, 'fmt ');\n view.setUint32(16, 16, true); // fmt chunk size\n view.setUint16(20, 1, true); // PCM\n view.setUint16(22, 1, true); // mono\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate, true); // byte rate\n view.setUint16(32, 1, true); // block align\n view.setUint16(34, 8, true); // 8 bits per sample\n writeStr(36, 'data');\n view.setUint32(40, numSamples, true);\n new Uint8Array(buffer, 44).fill(128); // 8-bit PCM silence = 128\n\n _silentWavUrl = URL.createObjectURL(new Blob([buffer], { type: 'audio/wav' }));\n return _silentWavUrl;\n}\n\nexport class Queue implements TrackQueueRef {\n private _tracks: Track[] = [];\n private readonly _actor;\n\n private readonly _onProgress?: (info: TrackInfo) => void;\n private readonly _onEnded?: () => void;\n private readonly _onPlayNextTrack?: (info: TrackInfo) => void;\n private readonly _onPlayPreviousTrack?: (info: TrackInfo) => void;\n private readonly _onStartNewTrack?: (info: TrackInfo) => void;\n private readonly _onError?: (error: Error) => void;\n private readonly _onPlayBlocked?: () => void;\n private readonly _onDebug?: (msg: string) => void;\n\n readonly playbackMethod: PlaybackMethod;\n\n private _volume: number;\n private _preloadNumTracks: number;\n private _playbackRate: number;\n\n /** Silent looping element that keeps the browser's MediaSession anchor alive. */\n private _mediaSessionAnchor: HTMLAudioElement | null = null;\n\n /** Index of the next track with a pre-scheduled gapless start, or null. */\n private _scheduledNextIndex: number | null = null;\n\n private _throttledUpdatePositionState = throttle(\n (duration: number, currentTime: number, playbackRate: number) =>\n updateMediaSessionPositionState(duration, currentTime, playbackRate),\n 1000,\n );\n\n constructor(options: GaplessOptions = {}) {\n const {\n tracks = [],\n onProgress,\n onEnded,\n onPlayNextTrack,\n onPlayPreviousTrack,\n onStartNewTrack,\n onError,\n onPlayBlocked,\n onDebug,\n playbackMethod = 'HYBRID',\n trackMetadata = [],\n volume: initialVolume = 1,\n preloadNumTracks = 2,\n playbackRate: initialPlaybackRate = 1,\n } = options;\n\n this._volume = Math.min(1, Math.max(0, initialVolume));\n this._preloadNumTracks = Math.max(0, preloadNumTracks);\n this._playbackRate = Math.min(4, Math.max(0.25, initialPlaybackRate));\n this.playbackMethod = playbackMethod;\n this._onProgress = onProgress;\n this._onEnded = onEnded;\n this._onPlayNextTrack = onPlayNextTrack;\n this._onPlayPreviousTrack = onPlayPreviousTrack;\n this._onStartNewTrack = onStartNewTrack;\n this._onError = onError;\n this._onPlayBlocked = onPlayBlocked;\n this._onDebug = onDebug;\n\n this._tracks = tracks.map(\n (url, i) =>\n new Track({\n trackUrl: url,\n index: i,\n queue: this,\n metadata: trackMetadata[i],\n })\n );\n\n // -----------------------------------------------------------------------\n // Wire up the queue machine with real action implementations.\n //\n // IMPORTANT: actions receive ({ context }) which reflects the in-progress\n // context (updated by prior assign() calls within the same transition).\n // Do NOT use this._actor.getSnapshot() inside actions — that returns the\n // pre-transition snapshot and won't reflect intermediate assign() updates.\n // -----------------------------------------------------------------------\n const machine = createQueueMachine({\n currentTrackIndex: 0,\n trackCount: this._tracks.length\n }).provide({\n actions: {\n deactivateCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n deactivateEndedTrack: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n activateAndPlayCurrent: ({ context }) => {\n const track = this._trackAt(context.currentTrackIndex);\n if (!track) return;\n track.activate();\n if (this._scheduledNextIndex !== track.index) {\n track.play();\n }\n },\n playOrContinueGapless: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (!cur) return;\n if (this._scheduledNextIndex !== cur.index) {\n cur.play();\n } else {\n this._scheduledNextIndex = null;\n this.onDebug(\n `onTrackEnded: gapless track ${cur.index} — sourceNode=${cur.hasSourceNode} isPlaying=${cur.isPlaying} machineState=${cur.machineState}`\n );\n cur.startProgressLoop();\n }\n },\n cancelAllGapless: () => this._cancelScheduledGapless(),\n notifyStartNewTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onStartNewTrack?.(cur.toInfo());\n },\n notifyPlayNextTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayNextTrack?.(cur.toInfo());\n },\n notifyPlayPreviousTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayPreviousTrack?.(cur.toInfo());\n },\n notifyEnded: () => this._onEnded?.(),\n updateMediaSessionMetadata: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) updateMediaSessionMetadata(cur.metadata);\n },\n preloadAhead: ({ context }) => {\n this._preloadAhead(context.currentTrackIndex);\n },\n playCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.play();\n },\n pauseCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.pause();\n },\n seekCurrent: ({ context, event }) => {\n const e = event as { type: 'SEEK'; time: number };\n this._trackAt(context.currentTrackIndex)?.seek(e.time);\n },\n seekCurrentToZero: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.seek(0);\n },\n scheduleGapless: ({ context }) => {\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n cancelScheduledGapless: () => {\n this._cancelScheduledGapless();\n },\n cancelAndRescheduleGapless: ({ context }) => {\n this._cancelScheduledGapless();\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n },\n });\n\n this._actor = createActor(machine);\n\n this._actor.subscribe((snapshot) => {\n const playing = snapshot.value === 'playing';\n updateMediaSessionPlaybackState(playing);\n if (playing) {\n this._startMediaSessionAnchor();\n } else {\n this._stopMediaSessionAnchor();\n }\n });\n\n this._actor.start();\n\n setupMediaSession({\n onPlay: () => this.play(),\n onPause: () => {\n if (this._actor.getSnapshot().value === 'playing') this.pause();\n },\n onNext: () => this.next(),\n onPrevious: () => this.previous(),\n onSeek: (t) => this.seek(t),\n });\n }\n\n // --------------------------------------------------------------------------\n // Public API\n // --------------------------------------------------------------------------\n\n play(): void {\n if (!this._currentTrack) return;\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n togglePlayPause(): void {\n if (this._actor.getSnapshot().value === 'playing') {\n this.pause();\n } else {\n this.play();\n }\n }\n\n next(): void {\n const snap = this._actor.getSnapshot();\n const nextIndex = snap.context.currentTrackIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n this._actor.send({ type: 'NEXT' });\n }\n\n previous(): void {\n const ct = this._currentTrack;\n if (ct && ct.currentTime > 8) {\n this._actor.send({ type: 'SEEK', time: 0 });\n return;\n }\n\n this._actor.send({ type: 'PREVIOUS' });\n }\n\n gotoTrack(index: number, playImmediately = false): void {\n if (index < 0 || index >= this._tracks.length) return;\n this.onDebug(\n `gotoTrack(${index}, playImmediately=${playImmediately}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`\n );\n this._actor.send({ type: 'GOTO', index, playImmediately });\n }\n\n seek(time: number): void {\n this._actor.send({ type: 'SEEK', time });\n }\n\n setVolume(volume: number): void {\n const clamped = Math.min(1, Math.max(0, volume));\n this._volume = clamped;\n for (const track of this._tracks) track.setVolume(clamped);\n }\n\n setPlaybackRate(rate: number): void {\n const clamped = Math.min(4, Math.max(0.25, rate));\n this._playbackRate = clamped;\n this._currentTrack?.setPlaybackRate(clamped);\n this._cancelScheduledGapless();\n const snap = this._actor.getSnapshot();\n if (snap.value === 'playing') {\n this._tryScheduleGapless(snap.context.currentTrackIndex);\n }\n }\n\n addTrack(url: string, options: AddTrackOptions = {}): void {\n const index = this._tracks.length;\n const metadata = options.metadata ?? ({} as TrackMetadata);\n this._tracks.push(\n new Track({\n trackUrl: url,\n index,\n queue: this,\n skipHEAD: options.skipHEAD,\n metadata,\n })\n );\n this._actor.send({ type: 'ADD_TRACK' });\n }\n\n removeTrack(index: number): void {\n if (index < 0 || index >= this._tracks.length) return;\n this._tracks[index].destroy();\n this._tracks.splice(index, 1);\n for (let i = index; i < this._tracks.length; i++) {\n (this._tracks[i] as unknown as { index: number }).index = i;\n }\n if (this._scheduledNextIndex === index) {\n this._scheduledNextIndex = null;\n } else if (this._scheduledNextIndex !== null && this._scheduledNextIndex > index) {\n this._scheduledNextIndex--;\n }\n this._actor.send({ type: 'REMOVE_TRACK', index });\n }\n\n resumeAudioContext(): Promise<void> {\n return _resumeAudioContext();\n }\n\n destroy(): void {\n this._stopMediaSessionAnchor();\n this._mediaSessionAnchor = null;\n for (const track of this._tracks) track.destroy();\n this._tracks = [];\n this._actor.stop();\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTrack(): TrackInfo | undefined {\n return this._currentTrack?.toInfo();\n }\n\n get currentTrackIndex(): number {\n return this._actor.getSnapshot().context.currentTrackIndex;\n }\n\n get tracks(): readonly TrackInfo[] {\n return this._tracks.map((t) => t.toInfo());\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().value === 'playing';\n }\n\n get isPaused(): boolean {\n return this._actor.getSnapshot().value === 'paused';\n }\n\n get volume(): number {\n return this._volume;\n }\n\n get preloadNumTracks(): number {\n return this._preloadNumTracks;\n }\n\n set preloadNumTracks(value: number) {\n this._preloadNumTracks = Math.max(0, value);\n const snap = this._actor.getSnapshot();\n if (snap.value === 'playing') {\n this._preloadAhead(snap.context.currentTrackIndex);\n }\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n /** Snapshot of the queue state machine (state name + context). For debugging. */\n get queueSnapshot(): { state: string; context: { currentTrackIndex: number; trackCount: number } } {\n const snap = this._actor.getSnapshot();\n return { state: snap.value as string, context: snap.context };\n }\n\n // --------------------------------------------------------------------------\n // TrackQueueRef — called by Track instances\n // --------------------------------------------------------------------------\n\n onTrackEnded(track: Track): void {\n const snap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded track=${track.index} queueState=${snap.value} curIdx=${snap.context.currentTrackIndex}`\n );\n // Compare object identity, not just index — after removeTrack(), a\n // different track may occupy the same index as the ended track.\n if (track !== this._trackAt(snap.context.currentTrackIndex)) return;\n\n this._actor.send({ type: 'TRACK_ENDED' });\n const newSnap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded after TRACK_ENDED → queueState=${newSnap.value} curIdx=${newSnap.context.currentTrackIndex}`\n );\n }\n\n onTrackBufferReady(track: Track): void {\n this._actor.send({ type: 'TRACK_LOADED', index: track.index });\n }\n\n onProgress(info: TrackInfo): void {\n if (info.index !== this._actor.getSnapshot().context.currentTrackIndex) return;\n if (!isNaN(info.duration)) {\n this._throttledUpdatePositionState(info.duration, info.currentTime, this._playbackRate);\n }\n this._onProgress?.(info);\n }\n\n onError(error: Error): void {\n this._onError?.(error);\n }\n\n onPlayBlocked(): void {\n this._actor.send({ type: 'PAUSE' });\n this._onPlayBlocked?.();\n }\n\n onPreloadReady(track: Track): void {\n const snap = this._actor.getSnapshot();\n if (track.index !== snap.context.currentTrackIndex) return;\n this._preloadAhead(snap.context.currentTrackIndex);\n }\n\n onDebug(msg: string): void {\n this._onDebug?.(msg);\n }\n\n // --------------------------------------------------------------------------\n // Private helpers\n // --------------------------------------------------------------------------\n\n /** Look up a track by index — safe for use inside machine actions. */\n private _trackAt(index: number): Track | undefined {\n return this._tracks[index];\n }\n\n private get _currentTrack(): Track | undefined {\n return this._tracks[this._actor.getSnapshot().context.currentTrackIndex];\n }\n\n private _preloadAhead(fromIndex: number): void {\n const cur = this._trackAt(fromIndex);\n\n // Bandwidth-contention gate: while the current track is playing via HTML5\n // and its own Web Audio buffer is still being fetched+decoded, holding\n // off on every next-track preload prevents two concurrent large MP3\n // downloads from delaying the current track's crossover (and therefore\n // the moment from which all future gapless transitions are\n // sample-accurate). When the current track's BUFFER_READY fires,\n // notifyBufferReady → TRACK_LOADED → preloadAhead re-runs, this gate\n // is no longer engaged, and next-track fetches proceed sequentially\n // as buffers complete.\n //\n // This lives here (not in the queue machine) because preloadAhead is\n // invoked as a transition action from ~7 different events, and the\n // gate's data source — the current track's machine state — isn't\n // owned by the queue machine. Pushing the check into every caller\n // would just duplicate the same read.\n if (\n cur != null &&\n cur.isPlaying &&\n cur.playbackType === 'HTML5' &&\n cur.webAudioLoadingState === 'LOADING'\n ) {\n this.onDebug(`_preloadAhead(${fromIndex}): deferring all — current track buffer still loading`);\n return;\n }\n\n // Speculative-load throttle: the immediate next track is always allowed\n // (it must be ready for gapless scheduling), but tracks beyond that are\n // deferred until the current track has played past min(duration*0.2, 15s)\n // — the point at which we have evidence the user is committing to\n // listening, not skipping.\n const curBelowThreshold =\n cur != null &&\n cur.isPlaying &&\n (() => {\n const threshold = isNaN(cur.duration) ? 15 : Math.min(cur.duration * 0.2, 15);\n return cur.currentTime < threshold;\n })();\n\n const limit = fromIndex + this._preloadNumTracks + 1;\n this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length} belowThreshold=${curBelowThreshold}`);\n for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {\n const t = this._tracks[i];\n if (i > fromIndex + 1 && curBelowThreshold) {\n this.onDebug(`_preloadAhead: deferring track ${i} (current at ${cur!.currentTime.toFixed(1)}s, below threshold)`);\n return;\n }\n if (!t.isBufferLoaded) {\n this.onDebug(`_preloadAhead: starting preload for track ${i}`);\n t.preload();\n return;\n } else {\n this.onDebug(`_preloadAhead: track ${i} already loaded`);\n }\n }\n }\n\n private _cancelScheduledGapless(): void {\n if (this._scheduledNextIndex === null) return;\n const track = this._trackAt(this._scheduledNextIndex);\n if (track) {\n track.cancelGaplessStart();\n this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`);\n }\n // Cancel any HTML5 gain mute that was scheduled on the current track,\n // but only if it's still in HTML5 state. After crossover the gain node\n // is managed by the crossfade ramp — cancelScheduledValues would wipe it.\n const cur = this._trackAt(this._actor.getSnapshot().context.currentTrackIndex);\n if (cur && cur.playbackType === 'HTML5') cur.cancelHtml5Mute();\n this._scheduledNextIndex = null;\n }\n\n private _tryScheduleGapless(curIndex: number): void {\n const ctx = getAudioContext();\n if (!ctx || this.playbackMethod === 'HTML5_ONLY') return;\n\n const nextIndex = curIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n const current = this._tracks[curIndex];\n const next = this._tracks[nextIndex];\n\n if (\n !current.isBufferLoaded ||\n !next.isBufferLoaded ||\n this._scheduledNextIndex === nextIndex ||\n !current.isPlaying\n )\n return;\n\n const endTime = this._computeTrackEndTime(current);\n if (endTime === null) return;\n\n if (endTime < ctx.currentTime + 0.01) return;\n\n const timeUntilEnd = endTime - ctx.currentTime;\n if (current.playbackType === 'HTML5' && timeUntilEnd > MAX_SCHEDULE_LOOKAHEAD) {\n this.onDebug(\n `_tryScheduleGapless: deferring — HTML5 track ${curIndex} has ${timeUntilEnd.toFixed(1)}s remaining (max lookahead=${MAX_SCHEDULE_LOOKAHEAD}s)`\n );\n return;\n }\n\n if (!next.scheduleGaplessStart(endTime)) {\n this.onDebug(\n `_tryScheduleGapless: track ${nextIndex} rejected SCHEDULE_GAPLESS (state=${next.machineState})`\n );\n return;\n }\n // When scheduling from an HTML5-clock prediction, mute the HTML5 gain\n // at endTime so a stalled/late HTML5 element is silenced rather than\n // overlapping the next track.\n if (current.playbackType === 'HTML5') {\n current.scheduleHtml5Mute(endTime);\n }\n this.onDebug(\n `_tryScheduleGapless: scheduled track ${nextIndex} at endTime=${endTime.toFixed(3)} (in ${(endTime - ctx.currentTime).toFixed(1)}s) curPlaybackType=${current.playbackType}`\n );\n this._scheduledNextIndex = nextIndex;\n }\n\n private _computeTrackEndTime(track: Track): number | null {\n const ctx = getAudioContext();\n if (!ctx || !track.isBufferLoaded) return null;\n\n // Web Audio playback: derive the end time from the source node's live\n // playback anchor. For an uninterrupted gapless chain this equals\n // scheduledStart + duration exactly, but unlike that formula it remains\n // correct after a pause/resume re-anchors the source at a later context\n // time. (Using the stale scheduled start here is how the next track got\n // scheduled mid-track, with both tracks audible simultaneously.)\n const anchoredEnd = track.playbackEndContextTime;\n if (anchoredEnd !== null) {\n return anchoredEnd > ctx.currentTime ? anchoredEnd : null;\n }\n\n const duration = track.duration;\n if (isNaN(duration)) return null;\n const remaining = (duration - track.currentTime) / this._playbackRate;\n if (remaining <= 0) return null;\n return ctx.currentTime + remaining;\n }\n\n private _startMediaSessionAnchor(): void {\n if (typeof Audio === 'undefined') return;\n if (!this._mediaSessionAnchor) {\n this._mediaSessionAnchor = new Audio(getSilentWavUrl());\n this._mediaSessionAnchor.loop = true;\n // Volume stays at default (1) — Chrome/Safari may ignore a zero-volume\n // element for media focus. The WAV content is already silence.\n }\n if (this._mediaSessionAnchor.paused) {\n this._mediaSessionAnchor.play().catch(() => {});\n }\n }\n\n private _stopMediaSessionAnchor(): void {\n if (this._mediaSessionAnchor && !this._mediaSessionAnchor.paused) {\n this._mediaSessionAnchor.pause();\n }\n }\n}\n","// ---------------------------------------------------------------------------\n// Singleton AudioContext manager\n//\n// A single AudioContext is shared across all Track instances. This is\n// essential: all AudioBufferSourceNodes must share the same context so that\n// audioContext.currentTime is a common monotonic clock, enabling\n// sample-accurate scheduling of back-to-back tracks.\n// ---------------------------------------------------------------------------\n\nconst isBrowser = typeof window !== 'undefined';\n\nlet _audioContext: AudioContext | null | undefined = undefined;\n\nfunction createAudioContext(): AudioContext | null {\n if (!isBrowser) return null;\n const Ctor =\n (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })\n .AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctor) return null;\n return new Ctor();\n}\n\n/**\n * Return the shared AudioContext if it has already been created, otherwise null.\n *\n * Does NOT create the context — call resumeAudioContext() from a user-gesture\n * handler to create and resume it. This prevents the browser warning:\n * \"An AudioContext was prevented from starting automatically.\"\n */\nexport function getAudioContext(): AudioContext | null {\n // undefined = never initialised (return null without creating)\n // null = explicitly disabled or unavailable\n // AudioContext = ready to use\n if (_audioContext === undefined) return null;\n return _audioContext;\n}\n\n/**\n * Create (if needed) and resume the AudioContext.\n * Must be called from a user-gesture handler (click, keydown, etc.).\n * Safe to call multiple times — subsequent calls are cheap no-ops.\n */\nexport function resumeAudioContext(): Promise<void> {\n if (_audioContext === undefined) {\n _audioContext = createAudioContext();\n }\n const ctx = _audioContext;\n if (ctx && ctx.state === 'suspended') {\n return ctx.resume();\n }\n return Promise.resolve();\n}\n\n/**\n * Reset the cached AudioContext — used in tests so each test gets a fresh mock.\n * @internal\n */\nexport function _resetAudioContext(): void {\n _audioContext = undefined;\n}\n\n/**\n * Directly set the cached AudioContext — used in tests to inject a mock instance.\n * @internal\n */\nexport function _setAudioContext(ctx: AudioContext | null): void {\n _audioContext = ctx;\n}\n","// ---------------------------------------------------------------------------\n// Media Session API integration\n//\n// Provides OS-level media controls (lock screen, notification shade, keyboard\n// media keys). Gracefully no-ops when the API is unavailable.\n// ---------------------------------------------------------------------------\n\nimport type { TrackMetadata } from '../types';\n\nconst hasMediaSession =\n typeof navigator !== 'undefined' && 'mediaSession' in navigator;\n\nexport interface MediaSessionHandlers {\n onPlay: () => void;\n onPause: () => void;\n onNext: () => void;\n onPrevious: () => void;\n onSeek: (time: number) => void;\n}\n\n/** Register Media Session action handlers. Call once after construction. */\nexport function setupMediaSession(handlers: MediaSessionHandlers): void {\n if (!hasMediaSession) return;\n const { mediaSession } = navigator;\n mediaSession.setActionHandler('play', handlers.onPlay);\n mediaSession.setActionHandler('pause', handlers.onPause);\n mediaSession.setActionHandler('nexttrack', handlers.onNext);\n mediaSession.setActionHandler('previoustrack', handlers.onPrevious);\n mediaSession.setActionHandler('seekto', (details) => {\n if (details.seekTime != null) handlers.onSeek(details.seekTime);\n });\n}\n\n/** Update the OS metadata display (title, artist, album, artwork). */\nexport function updateMediaSessionMetadata(metadata?: TrackMetadata): void {\n if (!hasMediaSession) return;\n if (!metadata) {\n navigator.mediaSession.metadata = null;\n return;\n }\n navigator.mediaSession.metadata = new MediaMetadata({\n title: metadata.title ?? '',\n artist: metadata.artist ?? '',\n album: metadata.album ?? '',\n artwork: metadata.artwork ?? [],\n });\n}\n\n/** Sync the OS playback state indicator (playing / paused). */\nexport function updateMediaSessionPlaybackState(\n isPlaying: boolean,\n): void {\n if (!hasMediaSession) return;\n navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';\n}\n\n/** Update the OS position/duration display (lock screen scrubber). */\nexport function updateMediaSessionPositionState(\n duration: number,\n position: number,\n playbackRate = 1,\n): void {\n if (!hasMediaSession) return;\n try {\n navigator.mediaSession.setPositionState({ duration, position, playbackRate });\n } catch {\n // Some browsers throw on invalid values (e.g. NaN duration)\n }\n}\n","// ---------------------------------------------------------------------------\n// QueueMachine — top-level queue state (xstate v5)\n//\n// States:\n// idle No tracks, or not yet started.\n// playing A track is actively playing.\n// paused Explicitly paused by the user.\n// ended The last track in the queue has finished.\n//\n// Root-level `on:` eliminates the handler duplication that plagued v2.\n//\n// Named actions (no-op defaults here, real implementations via .provide()):\n// Actions before assign() see the OLD context (e.g. deactivateCurrent\n// sees the old currentTrackIndex). Actions after assign() see the NEW\n// context (e.g. activateAndPlayCurrent sees the incremented index).\n// Implementations MUST use the ({ context }) parameter, NOT getSnapshot().\n// ---------------------------------------------------------------------------\n\nimport { setup, assign } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface QueueContext {\n currentTrackIndex: number;\n trackCount: number;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type QueueEvent =\n | { type: 'PLAY' }\n | { type: 'PAUSE' }\n | { type: 'TOGGLE' }\n | { type: 'NEXT' }\n | { type: 'PREVIOUS' }\n | { type: 'GOTO'; index: number; playImmediately?: boolean }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'ADD_TRACK' }\n | { type: 'REMOVE_TRACK'; index: number }\n | { type: 'TRACK_ENDED' }\n | { type: 'TRACK_LOADED'; index: number };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createQueueMachine(initialContext: QueueContext) {\n return setup({\n types: {\n context: {} as QueueContext,\n events: {} as QueueEvent,\n },\n guards: {\n hasNextTrack: ({ context }) => context.currentTrackIndex + 1 < context.trackCount,\n playImmediately: ({ event }) =>\n !!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,\n willBeEmpty: ({ context }) => context.trackCount <= 1,\n isRemovingCurrent: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n return e.index === context.currentTrackIndex;\n },\n },\n actions: {\n // --- named assign actions (avoids mixing inline assign with custom actions) ---\n incrementTrackCount: assign({\n trackCount: ({ context }) => context.trackCount + 1,\n }),\n decrementTrackCount: assign({\n trackCount: ({ context }) => Math.max(0, context.trackCount - 1),\n currentTrackIndex: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n const newCount = Math.max(0, context.trackCount - 1);\n if (e.index < context.currentTrackIndex) {\n return Math.max(0, context.currentTrackIndex - 1);\n }\n // Clamp to last valid index when removing at or after current\n return newCount > 0\n ? Math.min(context.currentTrackIndex, newCount - 1)\n : 0;\n },\n }),\n gotoTrackIndex: assign({\n currentTrackIndex: ({ event }) =>\n (event as { type: 'GOTO'; index: number }).index,\n }),\n advanceToNextTrack: assign({\n currentTrackIndex: ({ context }) => {\n const next = context.currentTrackIndex + 1;\n return next < context.trackCount ? next : context.currentTrackIndex;\n },\n }),\n goToPreviousTrack: assign({\n currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),\n }),\n advanceOnTrackEnd: assign({\n currentTrackIndex: ({ context }) =>\n Math.min(context.currentTrackIndex + 1, context.trackCount - 1),\n }),\n resetToFirstTrack: assign({\n currentTrackIndex: () => 0,\n }),\n // --- side-effect actions (no-op defaults, provided by Queue.ts) ---\n deactivateCurrent: () => {},\n deactivateEndedTrack: () => {},\n activateAndPlayCurrent: () => {},\n playOrContinueGapless: () => {},\n cancelAllGapless: () => {},\n notifyStartNewTrack: () => {},\n notifyPlayNextTrack: () => {},\n notifyPlayPreviousTrack: () => {},\n notifyEnded: () => {},\n updateMediaSessionMetadata: () => {},\n preloadAhead: () => {},\n playCurrent: () => {},\n pauseCurrent: () => {},\n seekCurrent: () => {},\n seekCurrentToZero: () => {},\n scheduleGapless: () => {},\n cancelScheduledGapless: () => {},\n cancelAndRescheduleGapless: () => {},\n },\n }).createMachine({\n id: 'queue',\n initial: 'idle',\n context: initialContext,\n\n // Global handlers — shared across all states\n on: {\n ADD_TRACK: {\n actions: 'incrementTrackCount',\n },\n REMOVE_TRACK: {\n actions: 'decrementTrackCount',\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle\n // -----------------------------------------------------------------\n idle: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // playing\n // -----------------------------------------------------------------\n playing: {\n on: {\n REMOVE_TRACK: [\n {\n guard: 'willBeEmpty',\n target: 'idle',\n actions: ['cancelAllGapless', 'decrementTrackCount'],\n },\n {\n guard: 'isRemovingCurrent',\n actions: [\n 'cancelAllGapless',\n 'decrementTrackCount',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: ['decrementTrackCount', 'cancelAndRescheduleGapless', 'preloadAhead'],\n },\n ],\n PAUSE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n TOGGLE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent', 'cancelAndRescheduleGapless'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'playing',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'playOrContinueGapless',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: [\n {\n // When the *current* track's buffer becomes ready while we are\n // already playing, that's a mid-stream HTML5 → Web Audio\n // crossover. Any existing gapless schedule was based on the old\n // HTML5-clock end-time prediction; cancel and re-schedule using\n // the now-authoritative WebAudio end time.\n guard: ({ context, event }) => event.index === context.currentTrackIndex,\n actions: ['cancelAndRescheduleGapless', 'preloadAhead'],\n },\n {\n actions: ['scheduleGapless', 'preloadAhead'],\n },\n ],\n },\n },\n\n // -----------------------------------------------------------------\n // paused\n // -----------------------------------------------------------------\n paused: {\n on: {\n REMOVE_TRACK: [\n {\n guard: 'willBeEmpty',\n target: 'idle',\n actions: ['cancelAllGapless', 'decrementTrackCount'],\n },\n {\n guard: 'isRemovingCurrent',\n actions: ['cancelAllGapless', 'decrementTrackCount', 'preloadAhead'],\n },\n {\n actions: ['decrementTrackCount', 'preloadAhead'],\n },\n ],\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n TOGGLE: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'paused',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // ended\n // -----------------------------------------------------------------\n ended: {\n on: {\n ADD_TRACK: {\n target: 'paused',\n actions: 'incrementTrackCount',\n },\n PLAY: {\n target: 'playing',\n actions: [\n 'resetToFirstTrack',\n 'playCurrent',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n 'scheduleGapless',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n },\n },\n },\n });\n}\n\nexport type QueueMachine = ReturnType<typeof createQueueMachine>;\n","// ---------------------------------------------------------------------------\n// Track — owns one audio track's Web Audio nodes and drives TrackMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor, fromPromise } from 'xstate';\nimport { getAudioContext, resumeAudioContext } from './utils/audioContext';\nimport { createTrackMachine } from './machines/track.machine';\nimport { fetchDecodeMachine } from './machines/fetchDecode.machine';\nimport type { TrackContext } from './machines/track.machine';\nimport type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType, PlaybackMethod } from './types';\n\nexport interface TrackQueueRef {\n onTrackEnded(track: Track): void;\n onTrackBufferReady(track: Track): void;\n onPreloadReady(track: Track): void;\n onProgress(info: TrackInfo): void;\n onError(error: Error): void;\n onPlayBlocked(): void;\n onDebug(msg: string): void;\n readonly volume: number;\n readonly playbackMethod: PlaybackMethod;\n readonly playbackRate: number;\n readonly currentTrackIndex: number;\n}\n\n/** How close to the end (in seconds) before we attempt gapless scheduling. */\nconst GAPLESS_SCHEDULE_LOOKAHEAD = 5;\n\n/** How many seconds into HTML5 playback before we preload the next track. */\nconst PRELOAD_DELAY = 15;\n\n/**\n * Crossfade duration (seconds) for the mid-stream HTML5 → Web Audio handoff.\n * Both sides of the crossfade run on AudioContext-clock gain ramps\n * (the HTML5 side via a GainNode after MediaElementAudioSourceNode), so\n * the join is sample-accurate. 30 ms is short enough to be inaudible as a\n * fade and long enough to mask any sample-level discontinuity between the\n * two streams.\n */\nconst CROSSOVER_FADE_SEC = 0.03;\n\n\nexport class Track {\n readonly index: number;\n readonly metadata: TrackMetadata;\n\n private _trackUrl: string;\n private _resolvedUrl: string;\n private readonly skipHEAD: boolean;\n /** Temporary holder between fetch and decode steps (unserializable — stays on Track class). */\n private _pendingArrayBuffer: ArrayBuffer | null = null;\n\n // ---- HTML5 Audio ---------------------------------------------------------\n readonly audio: HTMLAudioElement;\n\n // ---- Web Audio nodes -----------------------------------------------------\n private readonly _playbackMethod: PlaybackMethod;\n\n private get ctx(): AudioContext | null {\n if (this._playbackMethod === 'HTML5_ONLY') return null;\n const context = getAudioContext();\n if (context && !this.gainNode) {\n this.gainNode = context.createGain();\n this.gainNode.gain.value = this.audio.volume;\n this.gainNode.connect(context.destination);\n\n // Route the HTML5 element through the AudioContext via a\n // MediaElementAudioSourceNode + dedicated GainNode. With this routing,\n // the HTML5 side has its own AudioContext-clock gain we can ramp\n // sample-accurately during crossover, eliminating the click that an\n // abrupt audio.pause() leaves at the cut point. Volume control moves\n // entirely onto gainNode (master); we set audio.volume = 1 so the\n // browser-side and AudioContext-side gains don't multiply.\n //\n // createMediaElementSource severs the element's default audio output\n // for the rest of its life, so once this runs, all HTML5 playback\n // flows through the AudioContext. CORS: cross-origin audio without\n // Access-Control-Allow-Origin will produce silence through the\n // MediaElementAudioSourceNode path. If the call throws (rare; some\n // legacy implementations or double-attach), fall back to the\n // pre-routing behavior with audio.volume controlling HTML5 directly.\n try {\n this._mediaElementSource = context.createMediaElementSource(this.audio);\n this._html5GainNode = context.createGain();\n this._html5GainNode.gain.value = 1;\n this._mediaElementSource.connect(this._html5GainNode);\n this._html5GainNode.connect(this.gainNode);\n this.audio.volume = 1;\n } catch {\n this._mediaElementSource = null;\n this._html5GainNode = null;\n }\n }\n return context;\n }\n\n private gainNode: GainNode | null = null;\n private _mediaElementSource: MediaElementAudioSourceNode | null = null;\n private _html5GainNode: GainNode | null = null;\n private sourceNode: AudioBufferSourceNode | null = null;\n audioBuffer: AudioBuffer | null = null;\n /** AudioContext.currentTime at the start of the current playback segment. */\n /**\n * Offset (in seconds) between the decoded buffer's \"offset 0\" and the music's\n * \"offset 0\". Some MP3 files include ID3v2 metadata, encoder priming samples,\n * or container padding at the start; HTML5 audio elements skip past these\n * natively (audio.currentTime=0 means music start), but `decodeAudioData` in\n * some browsers includes them in the decoded buffer (buffer offset 0 = file\n * start, music actually starts at offset _bufferStartPaddingSec).\n *\n * Without this shift, calling source.start(when, audio.currentTime) plays\n * `audio.currentTime` seconds AHEAD of what HTML5 was just outputting,\n * sounding like a backward skip at crossover. We compute this once both\n * the buffer and audio.duration are known, and apply it as\n * source.start(when, trackTime + _bufferStartPaddingSec)\n * everywhere we read from the buffer. User-facing time (currentTime/duration\n * getters) continues to be reported in music-time, not buffer-time.\n */\n private _bufferStartPaddingSec = 0;\n private _bufferAlignmentMeasured = false;\n\n private _waRefCtxTime = 0;\n /** Track position (seconds) at the start of the current playback segment. */\n private _waRefTrackTime = 0;\n /** Track-time (seconds) frozen at the moment of the most recent pause. */\n private pausedAtTrackTime = 0;\n // ---- FSM -----------------------------------------------------------------\n private readonly _actor;\n\n // ---- Callbacks -----------------------------------------------------------\n private readonly queueRef: TrackQueueRef;\n private rafId: number | null = null;\n private _notifiedPreloadThreshold = false;\n\n constructor(opts: {\n trackUrl: string;\n index: number;\n queue: TrackQueueRef;\n skipHEAD?: boolean;\n metadata?: TrackMetadata;\n }) {\n this.index = opts.index;\n this._trackUrl = opts.trackUrl;\n this._resolvedUrl = opts.trackUrl;\n this.skipHEAD = opts.skipHEAD ?? false;\n this.metadata = opts.metadata ?? {};\n this.queueRef = opts.queue;\n\n // HTML5 Audio\n this.audio = new Audio();\n this.audio.preload = 'none';\n // crossOrigin must be set BEFORE src for MediaElementAudioSourceNode to\n // actually expose the element's audio to the AudioContext graph. Without\n // it, even servers that DO send CORS headers result in the\n // MediaElementSource being treated as cross-origin tainted, and the node\n // outputs silence — manifesting as \"HTML5 plays fine until WebAudio\n // takes over, then suddenly audible\". HTML5_ONLY mode skips this since\n // it doesn't route through AudioContext; setting crossOrigin there\n // would make non-CORS sources fail to load at all.\n if (opts.queue.playbackMethod !== 'HTML5_ONLY') {\n this.audio.crossOrigin = 'anonymous';\n }\n this.audio.src = this._trackUrl;\n this.audio.volume = opts.queue.volume;\n this.audio.controls = false;\n this.audio.onerror = () => {\n const code = this.audio.error?.code;\n if (code === 1) return;\n const msg = this.audio.error?.message ?? 'unknown';\n this.queueRef.onError(\n new Error(`HTML5 audio error on track ${this.index} (code ${code}): ${msg}`)\n );\n };\n this.audio.onended = () => {\n this.queueRef.onDebug(\n `audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n this._actor.send({ type: 'HTML5_ENDED' });\n };\n\n this._playbackMethod = opts.queue.playbackMethod;\n\n const initialContext: TrackContext = {\n trackUrl: this._trackUrl,\n resolvedUrl: this._trackUrl,\n skipHEAD: this.skipHEAD,\n playbackType: 'HTML5',\n webAudioLoadingState: 'NONE',\n isPlaying: false,\n scheduledStartContextTime: null,\n notifiedLookahead: false,\n fetchStarted: false,\n pendingPlay: false,\n };\n const machine = createTrackMachine(initialContext).provide({\n guards: {\n canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),\n isWebAudioOnly: () => this._playbackMethod === 'WEBAUDIO_ONLY',\n },\n actors: {\n fetchDecode: fetchDecodeMachine.provide({\n actors: {\n resolveUrl: fromPromise(async ({ signal }) => {\n const res = await fetch(this._trackUrl, { method: 'HEAD', signal });\n if (res.redirected && res.url) {\n // Cache the resolved URL so the WebAudio GET below uses it\n // directly (avoiding a second redirect round-trip). Do NOT\n // overwrite this.audio.src — the HTML5 element may currently\n // be streaming, and assigning a new src aborts playback,\n // resets audio.currentTime to 0, and reloads from the new\n // URL. The browser already handles the original URL's\n // redirect transparently for HTML5; updating src here causes\n // a perceived jump back to the start mid-playback (which the\n // user then notices later as a \"skip\" at crossover, since\n // audio.currentTime captured at crossover time reflects the\n // post-reset position rather than the user's actual progress).\n this._resolvedUrl = res.url;\n return res.url;\n }\n return null;\n }),\n fetchAudio: fromPromise(async ({ input, signal }) => {\n const { resolvedUrl } = input as { resolvedUrl: string };\n const res = await fetch(resolvedUrl, { signal });\n if (!res.ok) throw new Error(`HTTP ${res.status} for ${resolvedUrl}`);\n this._pendingArrayBuffer = await res.arrayBuffer();\n }),\n decodeAudio: fromPromise(async () => {\n const buf = this._pendingArrayBuffer;\n this._pendingArrayBuffer = null;\n if (!buf || !this.ctx) throw new Error('No ArrayBuffer or AudioContext');\n this.audioBuffer = await this.ctx.decodeAudioData(buf);\n this._maybeComputeBufferAlignment();\n // NOTE: do NOT notify the queue from here. The fetchDecode child\n // is about to sendParent('BUFFER_READY'), which the track machine\n // handles by transitioning state (and, in html5, by performing\n // the mid-stream crossover). If we notified the queue first, it\n // would observe a stale playbackType ('HTML5') and incorrectly\n // defer next-track preload via the HTML5 deferral guard. The\n // notification is emitted from the BUFFER_READY transitions in\n // track.machine, after the crossover/state update is in effect.\n }),\n },\n }),\n },\n actions: {\n triggerFetchForPendingPlay: () => {\n this.preload();\n resumeAudioContext();\n },\n playHtml5: () => this._playHtml5(),\n startSourceNode: () => {\n this._startSourceNode(this.pausedAtTrackTime);\n },\n crossoverHtml5ToWebAudio: ({ context }: { context: TrackContext }) => {\n this._crossoverHtml5ToWebAudio(context.isPlaying);\n },\n startScheduledSourceNode: ({ context }: { context: TrackContext }) => {\n const when = context.scheduledStartContextTime;\n if (when === null || !this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._maybeComputeBufferAlignment();\n this._stopSourceNode();\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.playbackRate.value = this.queueRef.playbackRate;\n this.sourceNode.connect(this.gainNode);\n // gainNode → destination connected once at ctx setup; do not re-connect\n // here or the gainNode's output is duplicated (Web Audio sums multiple\n // edges between the same pair of nodes).\n this.sourceNode.onended = this._handleWebAudioEnded;\n // Skip past any decoded-buffer start padding (ID3 tags, encoder\n // priming) so the gapless transition begins at music-time 0, not at\n // half a second of silence/garbage. _bufferStartPaddingSec is\n // computed at decode time via _ensureBufferAlignment.\n this.sourceNode.start(when, this._bufferStartPaddingSec);\n const bufRemaining = this.audioBuffer.duration - this._bufferStartPaddingSec;\n const schedRate = this.sourceNode.playbackRate.value || 1;\n if (bufRemaining > 0) {\n this.sourceNode.stop(when + bufRemaining / schedRate);\n }\n this._waRefCtxTime = when;\n this._waRefTrackTime = 0;\n this.queueRef.onDebug(\n `startScheduledSourceNode track=${this.index} when=${when.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(when - this.ctx.currentTime).toFixed(3)}s padding=${this._bufferStartPaddingSec.toFixed(3)}s`\n );\n },\n startProgressLoop: () => this.startProgressLoop(),\n pauseHtml5: () => this.audio.pause(),\n freezePausedTime: () => {\n const t = this.currentTime;\n this.pausedAtTrackTime = isFinite(t) ? t : 0;\n },\n stopSourceNode: () => this._stopSourceNode(),\n stopProgressLoop: () => this._stopProgressLoop(),\n reportProgress: () => {\n queueMicrotask(() => this.queueRef.onProgress(this.toInfo()));\n },\n seekHtml5: () => this._seekHtml5(),\n seekWebAudio: () => this._seekWebAudio(),\n resetHtml5Element: () => {\n this.audio.currentTime = 0;\n },\n resetTiming: () => {\n this._waRefCtxTime = 0;\n this._waRefTrackTime = 0;\n this.pausedAtTrackTime = 0;\n },\n notifyTrackEnded: () => {\n queueMicrotask(() => this.queueRef.onTrackEnded(this));\n },\n notifyBufferReady: () => {\n // Emitted from BUFFER_READY transitions so the queue observes the\n // post-transition state (e.g. WEBAUDIO after a crossover) rather\n // than the pre-transition state.\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n },\n },\n });\n this._actor = createActor(machine);\n this._actor.start();\n }\n\n // --------------------------------------------------------------------------\n // Public playback controls\n // --------------------------------------------------------------------------\n\n play(): void {\n this.queueRef.onDebug(\n `Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`\n );\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n seek(time: number): void {\n if (!isFinite(time)) return;\n const clamped = Math.max(0, isNaN(this.duration) ? time : Math.min(time, this.duration));\n this.pausedAtTrackTime = clamped;\n this._actor.send({ type: 'SEEK', time: clamped });\n }\n\n setVolume(v: number): void {\n const vol = Math.min(1, Math.max(0, v));\n // When the HTML5 element is routed through MediaElementAudioSourceNode,\n // master volume lives entirely on gainNode — keep audio.volume at 1 so the\n // browser-side and AudioContext-side gains don't multiply (giving v²).\n // Otherwise (HTML5_ONLY mode, or pre-ctx), audio.volume is the master.\n if (this._mediaElementSource) {\n this.audio.volume = 1;\n } else {\n this.audio.volume = vol;\n }\n if (this.gainNode) this.gainNode.gain.value = vol;\n this._actor.send({ type: 'SET_VOLUME', volume: vol });\n }\n\n setPlaybackRate(rate: number): void {\n // Freeze current track position at the old rate before switching.\n // Skip the anchor rewrite during the scheduling lead (when the source\n // is scheduled but ctx.currentTime hasn't reached _waRefCtxTime yet) —\n // the source hasn't started producing audio, so the anchors are still\n // correct for the future start. Rewriting them with a negative elapsed\n // time would corrupt playbackEndContextTime and cause overlap/gap.\n if (this.ctx && this.sourceNode && this._actor.getSnapshot().context.isPlaying\n && this.ctx.currentTime >= this._waRefCtxTime) {\n const oldRate = this.sourceNode.playbackRate.value;\n this._waRefTrackTime = this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * oldRate;\n this._waRefCtxTime = this.ctx.currentTime;\n }\n this.audio.playbackRate = rate;\n if (this.sourceNode) {\n this.sourceNode.playbackRate.value = rate;\n // Re-issue the hard stop ceiling at the new end time. The spec\n // allows repeated stop() calls (latest wins), so this overwrites\n // the stale stop scheduled at the old rate.\n if (this.audioBuffer && this.ctx) {\n const remaining = this.audioBuffer.duration - this._bufferStartPaddingSec - this._waRefTrackTime;\n if (remaining > 0) {\n try {\n this.sourceNode.stop(this._waRefCtxTime + remaining / rate);\n } catch { /* already stopped */ }\n }\n }\n }\n }\n\n preload(): void {\n this.queueRef.onDebug(\n `preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`\n );\n if (this._actor.getSnapshot().value === 'idle') {\n this._actor.send({ type: 'PRELOAD' });\n }\n if (this.audioBuffer) return;\n resumeAudioContext();\n if (!this.ctx) return;\n this._actor.send({ type: 'START_FETCH' });\n }\n\n seekToEnd(secondsFromEnd = 6): void {\n const dur = this.duration;\n if (!isNaN(dur) && dur > secondsFromEnd) {\n this.seek(dur - secondsFromEnd);\n }\n }\n\n activate(): void {\n this._actor.send({ type: 'ACTIVATE' });\n }\n\n deactivate(): void {\n this.queueRef.onDebug(\n `Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`\n );\n this._notifiedPreloadThreshold = false;\n this._actor.send({ type: 'DEACTIVATE' });\n this.queueRef.onDebug(\n `Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`\n );\n }\n\n destroy(): void {\n this.deactivate();\n this._pendingArrayBuffer = null;\n this.audioBuffer = null;\n this.gainNode?.disconnect();\n this.gainNode = null;\n this._actor.stop(); // Stops spawned fetchDecode child actor, aborting in-flight fetches\n }\n\n // --------------------------------------------------------------------------\n // Gapless scheduling (called by Queue)\n // --------------------------------------------------------------------------\n\n /** Schedule the HTML5 gain node to mute at `when` — safety valve so an\n * HTML5 element that runs past the predicted end is silenced instead of\n * overlapping the next track. No-op if the HTML5 gain path isn't active. */\n scheduleHtml5Mute(when: number): void {\n if (!this._html5GainNode || !this.ctx) return;\n this._html5GainNode.gain.setValueAtTime(1, when - 0.005);\n this._html5GainNode.gain.linearRampToValueAtTime(0, when);\n }\n\n /** Cancel any scheduled HTML5 gain mute (called when gapless is cancelled). */\n cancelHtml5Mute(): void {\n if (!this._html5GainNode || !this.ctx) return;\n this._html5GainNode.gain.cancelScheduledValues(this.ctx.currentTime);\n this._html5GainNode.gain.setValueAtTime(1, this.ctx.currentTime);\n }\n\n cancelGaplessStart(): void {\n const snap = this._actor.getSnapshot();\n if (snap.context.scheduledStartContextTime === null) return;\n this._actor.send({ type: 'CANCEL_GAPLESS' });\n }\n\n scheduleGaplessStart(when: number): boolean {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return false;\n const state = this._actor.getSnapshot().value;\n if (state !== 'idle' && state !== 'loading') return false;\n this._actor.send({ type: 'SCHEDULE_GAPLESS', when });\n return true;\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTime(): number {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') {\n if (!snap.context.isPlaying) return this.pausedAtTrackTime;\n if (!this.ctx) return 0;\n return Math.max(0, this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * this.queueRef.playbackRate);\n }\n return this.audio.currentTime;\n }\n\n get duration(): number {\n const snap = this._actor.getSnapshot();\n // When playing via HTML5, use audio.duration so it stays consistent\n // with audio.currentTime. audioBuffer.duration can differ (codec padding,\n // container overhead, VBR headers) causing gapless scheduling to start\n // the next track before the HTML5 element actually finishes.\n if (snap.value === 'html5' && !isNaN(this.audio.duration)) {\n return this.audio.duration;\n }\n if (this.audioBuffer) return this.audioBuffer.duration;\n return this.audio.duration;\n }\n\n get isPaused(): boolean {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') return !snap.context.isPlaying;\n return this.audio.paused;\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().context.isPlaying;\n }\n\n get trackUrl(): string {\n return this._resolvedUrl;\n }\n\n get playbackType(): PlaybackType {\n return this._actor.getSnapshot().context.playbackType;\n }\n\n get webAudioLoadingState(): WebAudioLoadingState {\n return this._actor.getSnapshot().context.webAudioLoadingState;\n }\n\n get hasSourceNode(): boolean {\n return this.sourceNode !== null;\n }\n\n get machineState(): string {\n return this._actor.getSnapshot().value as string;\n }\n\n get scheduledStartContextTime(): number | null {\n return this._actor.getSnapshot().context.scheduledStartContextTime;\n }\n\n /**\n * Context-clock time at which the currently playing Web Audio source will\n * reach the end of its buffer, derived from the live playback anchor\n * (_waRefCtxTime/_waRefTrackTime). The anchor is re-established every time\n * a source node starts (scheduled gapless start, resume, seek, crossover),\n * so this stays correct across interruptions — unlike\n * scheduledStartContextTime + duration, which goes stale the moment a\n * pause/resume re-anchors the source at a later context time. For an\n * uninterrupted gapless chain it is exact (the anchor IS the scheduled\n * start), so next-track scheduling stays sample-accurate.\n *\n * Null when no source node is active (idle/html5/loading, or paused).\n */\n get playbackEndContextTime(): number | null {\n const snap = this._actor.getSnapshot();\n if (snap.value !== 'webaudio' || !snap.context.isPlaying) return null;\n if (!this.sourceNode || !this.audioBuffer) return null;\n const rate = this.sourceNode.playbackRate.value || 1;\n const bufferRemaining =\n this.audioBuffer.duration - this._bufferStartPaddingSec - this._waRefTrackTime;\n return this._waRefCtxTime + bufferRemaining / rate;\n }\n\n get isBufferLoaded(): boolean {\n return this.audioBuffer !== null;\n }\n\n toInfo(): TrackInfo {\n return {\n index: this.index,\n currentTime: this.currentTime,\n duration: this.duration,\n isPlaying: this.isPlaying,\n isPaused: this.isPaused,\n volume: this.gainNode?.gain.value ?? this.audio.volume,\n trackUrl: this.trackUrl,\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n metadata: this.metadata,\n playbackRate: this.queueRef.playbackRate,\n machineState: this.machineState,\n };\n }\n\n // --------------------------------------------------------------------------\n // Private: HTML5 helpers\n // --------------------------------------------------------------------------\n\n private _playHtml5(): void {\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n this.audio.playbackRate = this.queueRef.playbackRate;\n const promise = this.audio.play();\n if (promise) {\n promise.catch((err: unknown) => {\n if (err instanceof Error && err.name === 'NotAllowedError') {\n this.queueRef.onPlayBlocked();\n } else if (err instanceof Error && err.name === 'AbortError') {\n // Browser aborted — element will recover on next play()\n } else {\n this.queueRef.onError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n }\n }\n\n private _seekHtml5(): void {\n const clamped = this.pausedAtTrackTime;\n if (!isFinite(clamped)) return;\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) {\n this.audio.currentTime = clamped;\n } else {\n this.audio.addEventListener(\n 'loadedmetadata',\n () => {\n this.audio.currentTime = clamped;\n },\n { once: true }\n );\n this.audio.load();\n }\n }\n\n // --------------------------------------------------------------------------\n // Private: Web Audio helpers\n // --------------------------------------------------------------------------\n\n /**\n * Mid-stream crossover: switch an actively-playing HTML5 track to Web Audio.\n *\n * Why this exists: we cannot reliably predict when an HTML5 <audio> element\n * will fire 'ended' from within the AudioContext clock. Any prediction is\n * at the mercy of the browser's audio pipeline (buffering stalls, codec\n * padding differences, clock drift between audio.currentTime and\n * ctx.currentTime over long sessions). Scheduling the next gapless track\n * against that prediction is how overlap bugs happen.\n *\n * Instead, as soon as the buffer is decoded, we hand playback off to Web\n * Audio while the track is still mid-song. From that point on, the track\n * and all subsequent gapless transitions live on a single clock\n * (AudioContext.currentTime), so scheduling is sample-accurate by\n * construction — no prediction involved.\n *\n * Ordering: pause the HTML5 element FIRST, then start the source node at\n * the captured offset. Pausing first ensures audio.currentTime is frozen\n * before we read it as the Web Audio start offset, so there's no brief\n * double-audio window at the crossover point.\n */\n private _crossoverHtml5ToWebAudio(wasPlaying: boolean): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n\n const offset = this.audio.currentTime;\n this.pausedAtTrackTime = isFinite(offset) ? offset : 0;\n\n if (!wasPlaying) {\n this.audio.pause();\n this.queueRef.onDebug(\n `crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=false`\n );\n return;\n }\n\n // Start the WebAudio source first to learn the exact context-clock time\n // it will begin producing audio (the \"when\" returned by _startSourceNode).\n // Then align the HTML5 fade-out to that same instant so both crossfade\n // halves overlap at constant-summed gain.\n const when = this._startSourceNode(this.pausedAtTrackTime, CROSSOVER_FADE_SEC);\n\n if (when !== null && this._html5GainNode) {\n this._html5GainNode.gain.cancelScheduledValues(when);\n this._html5GainNode.gain.setValueAtTime(1, when);\n this._html5GainNode.gain.linearRampToValueAtTime(0, when + CROSSOVER_FADE_SEC);\n const ctxRef = this.ctx;\n const html5GainRef = this._html5GainNode;\n const delayMs = (when - this.ctx.currentTime + CROSSOVER_FADE_SEC) * 1000 + 5;\n setTimeout(() => {\n this.audio.pause();\n if (ctxRef && html5GainRef) {\n html5GainRef.gain.cancelScheduledValues(ctxRef.currentTime);\n html5GainRef.gain.setValueAtTime(1, ctxRef.currentTime);\n }\n }, delayMs);\n } else if (!this._html5GainNode) {\n // No MediaElementSource path (CORS blocked or legacy browser).\n // Delay the HTML5 pause until the WebAudio source actually starts\n // at `when` so there's no silence gap during the scheduling lead.\n if (when !== null && this.ctx) {\n const delayMs = (when - this.ctx.currentTime) * 1000;\n setTimeout(() => {\n this.audio.volume = 0;\n this.audio.pause();\n this.audio.volume = 1;\n }, Math.max(0, delayMs));\n } else {\n this.audio.volume = 0;\n this.audio.pause();\n this.audio.volume = 1;\n }\n }\n\n this.queueRef.onDebug(\n `crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=true fade=${CROSSOVER_FADE_SEC}s mediaSource=${!!this._html5GainNode} when=${when?.toFixed(3) ?? 'null'}`\n );\n }\n\n /**\n * Reverted: alignment-based fixes (duration-delta and buffer-silence\n * scanning) reduced the perceived skip on archive.org files but did not\n * eliminate it, suggesting the residual gap isn't a buffer/timeline\n * alignment problem at all. Leaving _bufferStartPaddingSec at 0 (no shift)\n * until we have a confirmed root cause; the field and call sites are kept\n * so we can re-introduce a fix without churning the source-start code.\n */\n private _maybeComputeBufferAlignment(): void {\n if (!this.audioBuffer) return;\n if (this._bufferAlignmentMeasured) return;\n this._bufferStartPaddingSec = 0;\n this._bufferAlignmentMeasured = true;\n this.queueRef.onDebug(\n `_maybeComputeBufferAlignment track=${this.index} bufferDur=${this.audioBuffer.duration.toFixed(3)}s html5Dur=${isNaN(this.audio.duration) ? 'NaN' : this.audio.duration.toFixed(3) + 's'} (alignment shift disabled — see comment)`\n );\n }\n\n private _startSourceNode(offset: number, fadeInSec = 0): number | null {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return null;\n this._maybeComputeBufferAlignment();\n this._stopSourceNode();\n\n const wasSuspended = this.ctx.state === 'suspended';\n if (wasSuspended) {\n this.ctx.resume();\n }\n\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.playbackRate.value = this.queueRef.playbackRate;\n\n // Schedule the source at an explicit future time rather than start(0).\n // start(0) dispatches to the audio render thread asynchronously — the\n // actual start lags the main-thread currentTime read by up to one\n // hardware callback period (5-100 ms depending on device). That δ makes\n // playbackEndContextTime compute an end time δ too early, causing the\n // next gapless track to overlap briefly. Using start(when) with an\n // explicit when that matches _waRefCtxTime eliminates the mismatch.\n const lead = wasSuspended\n ? 0.15\n : Math.max(0.02, 2 * ((this.ctx as unknown as { baseLatency?: number }).baseLatency || 0) + 0.01);\n const when = this.ctx.currentTime + lead;\n\n // Advance the buffer start position by the lead so the source begins\n // from where playback will actually be at `when`, not where it was at\n // the moment we read currentTime. This keeps the currentTime formula\n // (waRefTrackTime + (ctx.currentTime - waRefCtxTime) * rate) equal to\n // `offset` immediately after the call without accumulating drift across\n // pause/resume cycles. During crossover it also aligns the WebAudio\n // source to where the HTML5 element will be at the fade point.\n // The skipped interval (≤20 ms at 1× rate) is below audible threshold.\n const rate = this.queueRef.playbackRate;\n const maxOffset = this.audioBuffer.duration - this._bufferStartPaddingSec;\n const effectiveOffset = Math.min(offset + lead * rate, maxOffset);\n\n if (fadeInSec > 0) {\n const fadeNode = this.ctx.createGain();\n fadeNode.gain.setValueAtTime(0, when);\n fadeNode.gain.linearRampToValueAtTime(1, when + fadeInSec);\n this.sourceNode.connect(fadeNode);\n fadeNode.connect(this.gainNode);\n } else {\n this.sourceNode.connect(this.gainNode);\n }\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this._waRefCtxTime = when;\n this._waRefTrackTime = effectiveOffset;\n this.sourceNode.start(when, effectiveOffset + this._bufferStartPaddingSec);\n\n // Hard ceiling: stop the source at its computed end time so any\n // platform-level late start (render thread couldn't honor `when`)\n // produces an inaudible truncation instead of overlap with the next track.\n const bufRemaining = this.audioBuffer.duration - this._bufferStartPaddingSec - effectiveOffset;\n if (bufRemaining > 0) {\n this.sourceNode.stop(when + bufRemaining / rate);\n }\n return when;\n }\n\n private _stopSourceNode(): void {\n if (!this.sourceNode) return;\n this.sourceNode.onended = null;\n try {\n this.sourceNode.stop();\n } catch {\n /* already stopped */\n }\n try {\n this.sourceNode.disconnect();\n } catch {\n /* already disconnected */\n }\n this.sourceNode = null;\n }\n\n private _seekWebAudio(): void {\n const snap = this._actor.getSnapshot();\n const wasPlaying = snap.context.isPlaying;\n const clamped = this.pausedAtTrackTime;\n this._stopSourceNode();\n if (wasPlaying) {\n this._startSourceNode(clamped);\n }\n }\n\n private _handleWebAudioEnded = (): void => {\n this.queueRef.onDebug(\n `_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n if (!this.sourceNode) return;\n this._actor.send({ type: 'WEBAUDIO_ENDED' });\n };\n\n // --------------------------------------------------------------------------\n // Private: progress loop (requestAnimationFrame)\n // --------------------------------------------------------------------------\n\n startProgressLoop(): void {\n if (this.rafId !== null) return;\n const loop = () => {\n if (this.isPaused || !this.isPlaying) {\n this.rafId = null;\n return;\n }\n this.queueRef.onProgress(this.toInfo());\n\n const remaining = this.duration - this.currentTime;\n const snap = this._actor.getSnapshot();\n if (\n !snap.context.notifiedLookahead &&\n !isNaN(remaining) &&\n remaining <= GAPLESS_SCHEDULE_LOOKAHEAD\n ) {\n this._actor.send({ type: 'LOOKAHEAD_REACHED' });\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }\n\n const preloadThreshold = isNaN(this.duration) ? PRELOAD_DELAY : Math.min(this.duration * 0.2, PRELOAD_DELAY);\n if (!this._notifiedPreloadThreshold && this.currentTime >= preloadThreshold) {\n this._notifiedPreloadThreshold = true;\n queueMicrotask(() => this.queueRef.onPreloadReady(this));\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n }\n\n private _stopProgressLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n}\n","// ---------------------------------------------------------------------------\n// TrackMachine — per-track audio state (xstate v5)\n//\n// States:\n// idle Initial state. Audio nodes not yet initialised.\n// html5 HTML5 Audio is playing. Web Audio fetch+decode is in progress.\n// loading Track is preloaded (not yet playing). Decode in progress.\n// webaudio AudioBufferSourceNode is the active output.\n//\n// Design invariant — \"Web Audio always wins as soon as the buffer is ready\":\n// When a track's buffer finishes decoding (BUFFER_READY) while the track is\n// playing via HTML5, we hand playback off to Web Audio mid-stream. The\n// HTML5 element is paused, and an AudioBufferSourceNode is started at the\n// exact offset the HTML5 element was at. From that instant on, the track\n// (and every gapless transition that follows it) lives on one clock\n// (AudioContext.currentTime).\n//\n// This is fundamental to gapless correctness: we cannot predict when an\n// HTML5 element will fire 'ended' from the AudioContext clock — the two\n// run on independent clocks with independent jitter (buffering stalls,\n// codec padding differences, long-session drift). Any gapless scheduling\n// that crosses those clocks is a prediction, and predictions are how\n// overlap bugs happen. By moving the current track to Web Audio as soon\n// as we can, all future gapless transitions are WebAudio→WebAudio and\n// sample-accurate by construction.\n//\n// DEACTIVATE transitions from every playing state land in idle, so a\n// deactivated track with a loaded buffer is always idle+LOADED — ready\n// for Web Audio on re-play.\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, spawnChild } from 'xstate';\nimport { fetchDecodeMachine } from './fetchDecode.machine';\nimport type { WebAudioLoadingState, PlaybackType } from '../types';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface TrackContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n playbackType: PlaybackType;\n webAudioLoadingState: WebAudioLoadingState;\n isPlaying: boolean;\n scheduledStartContextTime: number | null;\n notifiedLookahead: boolean;\n fetchStarted: boolean;\n /** True when PLAY was received in webAudioOnly mode before buffer is ready. */\n pendingPlay: boolean;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type TrackEvent =\n | { type: 'ACTIVATE' }\n | { type: 'DEACTIVATE' }\n | { type: 'PRELOAD' }\n | { type: 'BUFFER_LOADING' }\n | { type: 'PLAY' }\n | { type: 'PLAY_WEBAUDIO' }\n | { type: 'PAUSE' }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'BUFFER_READY' }\n | { type: 'BUFFER_ERROR' }\n | { type: 'HTML5_ENDED' }\n | { type: 'WEBAUDIO_ENDED' }\n | { type: 'URL_RESOLVED'; url: string }\n | { type: 'START_FETCH' }\n | { type: 'SCHEDULE_GAPLESS'; when: number }\n | { type: 'CANCEL_GAPLESS' }\n | { type: 'LOOKAHEAD_REACHED' };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createTrackMachine(initialContext: TrackContext) {\n return setup({\n types: {\n context: {} as TrackContext,\n events: {} as TrackEvent,\n },\n actors: {\n fetchDecode: fetchDecodeMachine,\n },\n guards: {\n canPlayWebAudio: () => false,\n isWebAudioOnly: () => false,\n canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && !context.fetchStarted,\n },\n actions: {\n playHtml5: () => {},\n startSourceNode: () => {},\n crossoverHtml5ToWebAudio: () => {},\n notifyBufferReady: () => {},\n startScheduledSourceNode: () => {},\n startProgressLoop: () => {},\n pauseHtml5: () => {},\n freezePausedTime: () => {},\n stopSourceNode: () => {},\n stopProgressLoop: () => {},\n reportProgress: () => {},\n seekHtml5: () => {},\n seekWebAudio: () => {},\n resetHtml5Element: () => {},\n resetTiming: () => {},\n notifyTrackEnded: () => {},\n triggerFetchForPendingPlay: () => {},\n setPendingPlay: assign({ pendingPlay: () => true }),\n clearPendingPlay: assign({ pendingPlay: () => false }),\n setIsPlaying: assign({ isPlaying: () => true }),\n clearIsPlaying: assign({ isPlaying: () => false }),\n setLoadingState: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),\n setLoadedState: assign({ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState }),\n setErrorState: assign({ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState }),\n clearScheduleAndLookahead: assign({ scheduledStartContextTime: () => null, notifiedLookahead: () => false }),\n setPlayingWebAudio: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n setScheduledGapless: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n scheduledStartContextTime: ({ event }) =>\n (event as { type: 'SCHEDULE_GAPLESS'; when: number }).when,\n }),\n clearPlayingAndSchedule: assign({\n isPlaying: () => false,\n scheduledStartContextTime: () => null,\n notifiedLookahead: () => false,\n }),\n setNotifiedLookahead: assign({ notifiedLookahead: () => true }),\n setResolvedUrl: assign({\n resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,\n }),\n clearScheduledStart: assign({ scheduledStartContextTime: () => null }),\n setPlayingWebAudioType: assign({\n isPlaying: () => true,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n setPlaybackTypeWebAudio: assign({\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n clearNotifiedLookahead: assign({\n notifiedLookahead: () => false,\n }),\n },\n }).createMachine({\n id: 'track',\n initial: 'idle',\n context: initialContext,\n\n on: {\n START_FETCH: {\n guard: 'canStartFetch',\n actions: [\n assign({\n webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState,\n fetchStarted: () => true,\n }),\n spawnChild('fetchDecode', {\n id: 'fetchDecode',\n input: ({ context }) => ({\n trackUrl: context.trackUrl,\n resolvedUrl: context.resolvedUrl,\n skipHEAD: context.skipHEAD,\n }),\n }),\n ],\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle: constructed but not started\n // -----------------------------------------------------------------\n idle: {\n on: {\n HTML5_ENDED: {\n actions: ['notifyTrackEnded'],\n },\n DEACTIVATE: {\n actions: [\n 'pauseHtml5',\n 'resetHtml5Element',\n 'resetTiming',\n 'stopProgressLoop',\n 'clearScheduleAndLookahead',\n 'clearPendingPlay',\n ],\n },\n ACTIVATE: {\n actions: [\n 'resetTiming',\n 'resetHtml5Element',\n 'clearScheduleAndLookahead',\n 'clearPendingPlay',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n guard: 'isWebAudioOnly',\n actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],\n },\n {\n target: 'html5',\n // triggerFetchForPendingPlay also kicks off fetch+decode for the\n // CURRENT track, not just the next one. That way BUFFER_READY\n // fires while we're in html5, and crossoverHtml5ToWebAudio can\n // hand the active track over to Web Audio mid-stream. The\n // canStartFetch guard inside START_FETCH makes this a no-op if\n // a fetch is already in flight (e.g. PLAY from loading state).\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop', 'triggerFetchForPendingPlay'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n PRELOAD: { target: 'loading' },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: [\n {\n guard: ({ context }: { context: TrackContext }) => context.pendingPlay,\n target: 'webaudio',\n actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop', 'notifyBufferReady'],\n },\n {\n actions: ['setLoadedState', 'notifyBufferReady'],\n },\n ],\n BUFFER_ERROR: {\n actions: ['setErrorState', 'clearPendingPlay'],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // html5: HTML5 Audio is playing; WebAudio decode may be in progress\n // -----------------------------------------------------------------\n html5: {\n on: {\n PAUSE: {\n actions: ['clearIsPlaying', 'pauseHtml5', 'stopProgressLoop', 'reportProgress'],\n },\n PLAY: {\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: 'setPlayingWebAudio',\n },\n // Mid-stream crossover to Web Audio. Running `crossoverHtml5ToWebAudio`\n // synchronously captures audio.currentTime, pauses the HTML5 element,\n // and starts a Web Audio source node at that exact offset. After the\n // transition completes we're in webaudio state with isPlaying preserved\n // (true if the track was playing, false if paused), so a subsequent\n // PLAY in webaudio state will resume from pausedAtTrackTime.\n //\n // clearNotifiedLookahead resets the gapless lookahead flag so the\n // webaudio progress loop can re-trigger scheduling with the accurate\n // shared-clock end time, replacing any stale HTML5-clock prediction.\n BUFFER_READY: {\n target: 'webaudio',\n actions: [\n 'setLoadedState',\n 'crossoverHtml5ToWebAudio',\n 'setPlaybackTypeWebAudio',\n 'clearNotifiedLookahead',\n 'notifyBufferReady',\n ],\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n SEEK: {\n actions: [\n 'seekHtml5',\n 'reportProgress',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n HTML5_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'pauseHtml5',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'pauseHtml5', 'resetHtml5Element', 'resetTiming', 'stopProgressLoop'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // loading: preloading in background (not the active track yet)\n // -----------------------------------------------------------------\n loading: {\n on: {\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: [\n {\n guard: ({ context }: { context: TrackContext }) => context.pendingPlay,\n target: 'webaudio',\n actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop', 'notifyBufferReady'],\n },\n {\n target: 'idle',\n actions: ['setLoadedState', 'notifyBufferReady'],\n },\n ],\n BUFFER_ERROR: {\n target: 'idle',\n actions: ['setErrorState', 'clearPendingPlay'],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n guard: 'isWebAudioOnly',\n actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],\n },\n {\n target: 'html5',\n // triggerFetchForPendingPlay also kicks off fetch+decode for the\n // CURRENT track, not just the next one. That way BUFFER_READY\n // fires while we're in html5, and crossoverHtml5ToWebAudio can\n // hand the active track over to Web Audio mid-stream. The\n // canStartFetch guard inside START_FETCH makes this a no-op if\n // a fetch is already in flight (e.g. PLAY from loading state).\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop', 'triggerFetchForPendingPlay'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'clearPendingPlay',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'clearPendingPlay', 'resetTiming'],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // webaudio: AudioBufferSourceNode is driving output\n // -----------------------------------------------------------------\n webaudio: {\n on: {\n // clearScheduledStart: once the source node is stopped, the\n // scheduled start time no longer describes anything real — resume\n // re-anchors the source at a later context time, so any end-time\n // math based on scheduledStartContextTime + duration would land\n // mid-track (scheduling the next track to start while this one is\n // still audible).\n PAUSE: {\n actions: [\n 'clearIsPlaying',\n 'freezePausedTime',\n 'clearScheduledStart',\n 'stopSourceNode',\n 'stopProgressLoop',\n 'reportProgress',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n actions: ['setIsPlaying', 'startSourceNode', 'startProgressLoop'],\n },\n {\n actions: 'setIsPlaying',\n },\n ],\n PLAY_WEBAUDIO: {\n actions: 'setPlayingWebAudioType',\n },\n SEEK: {\n actions: [\n 'clearScheduledStart',\n 'seekWebAudio',\n 'reportProgress',\n ],\n },\n SET_VOLUME: {},\n CANCEL_GAPLESS: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'stopProgressLoop',\n 'resetTiming',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n WEBAUDIO_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n // Bug #3 fix: DEACTIVATE from webaudio → idle (was staying in webaudio)\n DEACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'resetTiming',\n 'resetHtml5Element',\n 'stopProgressLoop',\n ],\n },\n },\n },\n },\n });\n}\n\nexport type TrackMachine = ReturnType<typeof createTrackMachine>;\n","// ---------------------------------------------------------------------------\n// FetchDecodeMachine — child actor for the fetch + decode pipeline\n//\n// States: resolvingUrl → fetching → decoding → done | error\n//\n// Spawned by TrackMachine via START_FETCH. Sends BUFFER_READY, BUFFER_ERROR,\n// and URL_RESOLVED back to the parent — the same events TrackMachine already\n// handles. Promise implementations are no-op defaults, provided by Track.ts\n// via .provide().\n//\n// xstate v5 automatically passes an AbortSignal to fromPromise actors, so\n// when the parent stops (destroy), in-flight fetches are aborted for free.\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, sendParent, fromPromise } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface FetchDecodeContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n}\n\n// ---- Machine ---------------------------------------------------------------\n\nexport const fetchDecodeMachine = setup({\n types: {\n context: {} as FetchDecodeContext,\n input: {} as FetchDecodeContext,\n },\n actors: {\n resolveUrl: fromPromise<string | null, { trackUrl: string }>(async () => null),\n fetchAudio: fromPromise<void, { resolvedUrl: string }>(async () => {}),\n decodeAudio: fromPromise<void, void>(async () => {}),\n },\n guards: {\n shouldSkipHEAD: ({ context }) => context.skipHEAD,\n },\n}).createMachine({\n id: 'fetchDecode',\n initial: 'resolvingUrl',\n context: {\n trackUrl: '',\n resolvedUrl: '',\n skipHEAD: false,\n },\n entry: assign(({ event }) => {\n const { input } = event as { type: string; input: FetchDecodeContext };\n return {\n trackUrl: input.trackUrl,\n resolvedUrl: input.resolvedUrl,\n skipHEAD: input.skipHEAD,\n };\n }),\n\n states: {\n // -----------------------------------------------------------------\n // resolvingUrl: HEAD request to resolve redirects\n // -----------------------------------------------------------------\n resolvingUrl: {\n always: {\n guard: 'shouldSkipHEAD',\n target: 'fetching',\n },\n invoke: {\n id: 'resolveUrl',\n src: 'resolveUrl',\n input: ({ context }) => ({ trackUrl: context.trackUrl }),\n onDone: {\n target: 'fetching',\n actions: [\n assign({\n resolvedUrl: ({ event, context }) => event.output ?? context.resolvedUrl,\n skipHEAD: () => true,\n }),\n sendParent(({ event, context }) => {\n const url = event.output;\n return { type: 'URL_RESOLVED' as const, url: url ?? context.resolvedUrl };\n }),\n ],\n },\n onError: {\n // HEAD failed — non-fatal, fall back to original URL\n target: 'fetching',\n actions: assign({ skipHEAD: () => true }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // fetching: GET the audio data\n // -----------------------------------------------------------------\n fetching: {\n invoke: {\n id: 'fetchAudio',\n src: 'fetchAudio',\n input: ({ context }) => ({ resolvedUrl: context.resolvedUrl }),\n onDone: 'decoding',\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // decoding: decodeAudioData on the ArrayBuffer\n // -----------------------------------------------------------------\n decoding: {\n invoke: {\n id: 'decodeAudio',\n src: 'decodeAudio',\n input: () => {},\n onDone: {\n target: 'done',\n actions: sendParent({ type: 'BUFFER_READY' }),\n },\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // Terminal states\n // -----------------------------------------------------------------\n done: { type: 'final' },\n error: { type: 'final' },\n },\n});\n\nexport type FetchDecodeMachine = typeof fetchDecodeMachine;\n","/** Returns a throttled version of `fn` that fires at most once per `ms`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(\n fn: T,\n ms: number,\n): (...args: Parameters<T>) => void {\n let last = 0;\n return (...args: Parameters<T>) => {\n const now = performance.now();\n if (now - last < ms) return;\n last = now;\n fn(...args);\n };\n}\n"],"mappings":"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,mBACA,qBACA,yBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,SAAU,CACR,QAAS,CACP,oBACA,mBACA,oBACA,yBACA,sBACA,0BACA,6BACA,cACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,QAAS,CACP,oBACA,mBACA,iBACA,yBACA,sBACA,6BACA,cACF,CACF,EACA,CACE,QAAS,CACP,oBACA,mBACA,iBACA,oBACA,cACF,CACF,CACF,EACA,KAAM,CACJ,QAAS,CAAC,cAAe,4BAA4B,CACvD,EACA,YAAa,CACX,CACE,MAAO,eACP,OAAQ,UACR,QAAS,CACP,uBACA,oBACA,wBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,CACE,OAAQ,QACR,QAAS,CAAC,uBAAwB,aAAa,CACjD,CACF,EACA,aAAc,CACZ,CAME,MAAO,CAAC,CAAE,QAAAC,EAAS,MAAAC,CAAM,IAAMA,EAAM,QAAUD,EAAQ,kBACvD,QAAS,CAAC,6BAA8B,cAAc,CACxD,EACA,CACE,QAAS,CAAC,kBAAmB,cAAc,CAC7C,CACF,CACF,CACF,EAKA,OAAQ,CACN,GAAI,CACF,aAAc,CACZ,CACE,MAAO,cACP,OAAQ,OACR,QAAS,CAAC,mBAAoB,qBAAqB,CACrD,EACA,CACE,MAAO,oBACP,QAAS,CAAC,mBAAoB,sBAAuB,cAAc,CACrE,EACA,CACE,QAAS,CAAC,sBAAuB,cAAc,CACjD,CACF,EACA,KAAM,CACJ,OAAQ,UACR,QAAS,CAAC,cAAe,6BAA8B,eAAgB,iBAAiB,CAC1F,EACA,OAAQ,CACN,OAAQ,UACR,QAAS,CAAC,cAAe,6BAA8B,eAAgB,iBAAiB,CAC1F,EACA,KAAM,CACJ,QAAS,CACP,oBACA,mBACA,qBACA,yBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,SAAU,CACR,QAAS,CACP,oBACA,mBACA,oBACA,yBACA,sBACA,0BACA,6BACA,cACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,UACR,QAAS,CACP,oBACA,mBACA,iBACA,yBACA,sBACA,6BACA,cACF,CACF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","names":["createActor","isBrowser","_audioContext","createAudioContext","Ctor","getAudioContext","resumeAudioContext","ctx","hasMediaSession","setupMediaSession","handlers","mediaSession","details","updateMediaSessionMetadata","metadata","updateMediaSessionPlaybackState","isPlaying","updateMediaSessionPositionState","duration","position","playbackRate","setup","assign","createQueueMachine","initialContext","context","event","e","newCount","next","createActor","fromPromise","setup","assign","spawnChild","setup","assign","sendParent","fromPromise","fetchDecodeMachine","context","event","input","createTrackMachine","initialContext","setup","fetchDecodeMachine","context","assign","event","spawnChild","GAPLESS_SCHEDULE_LOOKAHEAD","PRELOAD_DELAY","CROSSOVER_FADE_SEC","Track","context","getAudioContext","opts","_a","_b","code","msg","initialContext","machine","createTrackMachine","fetchDecodeMachine","fromPromise","signal","res","input","resolvedUrl","buf","resumeAudioContext","when","bufRemaining","schedRate","t","createActor","time","clamped","v","vol","rate","oldRate","remaining","secondsFromEnd","dur","state","snap","bufferRemaining","promise","err","wasPlaying","offset","ctxRef","html5GainRef","delayMs","fadeInSec","wasSuspended","lead","maxOffset","effectiveOffset","fadeNode","loop","preloadThreshold","throttle","fn","ms","last","args","now","MAX_SCHEDULE_LOOKAHEAD","_silentWavUrl","getSilentWavUrl","sampleRate","numSamples","fileSize","buffer","view","writeStr","off","s","i","Queue","throttle","duration","currentTime","playbackRate","updateMediaSessionPositionState","options","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","onError","onPlayBlocked","onDebug","playbackMethod","trackMetadata","initialVolume","preloadNumTracks","initialPlaybackRate","url","Track","machine","createQueueMachine","context","_a","track","cur","updateMediaSessionMetadata","event","e","createActor","snapshot","playing","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","rate","snap","metadata","resumeAudioContext","value","newSnap","info","error","msg","fromIndex","curBelowThreshold","threshold","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","timeUntilEnd","anchoredEnd","remaining"]}
|
|
1
|
+
{"version":3,"sources":["../src/Queue.ts","../src/utils/audioContext.ts","../src/utils/mediaSession.ts","../src/machines/queue.machine.ts","../src/Track.ts","../src/machines/track.machine.ts","../src/machines/fetchDecode.machine.ts","../src/utils/throttle.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Queue — public API class; orchestrates tracks via QueueMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor } from 'xstate';\nimport { getAudioContext, resumeAudioContext as _resumeAudioContext } from './utils/audioContext';\nimport {\n setupMediaSession,\n updateMediaSessionMetadata,\n updateMediaSessionPlaybackState,\n updateMediaSessionPositionState,\n} from './utils/mediaSession';\nimport { createQueueMachine } from './machines/queue.machine';\nimport { Track } from './Track';\nimport type { TrackQueueRef } from './Track';\nimport { throttle } from './utils/throttle';\nimport type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata, PlaybackMethod } from './types';\n\nconst MAX_SCHEDULE_LOOKAHEAD = 5;\n\n/**\n * Lazy singleton Blob URL for a 10-second silent WAV.\n * Chrome requires a media element with >= 5 s intrinsic duration to treat it\n * as a \"controllable\" media session. Generated once at first use; 8 kHz mono\n * 8-bit keeps it ~80 KB.\n */\nlet _silentWavUrl: string | null = null;\nfunction getSilentWavUrl(): string {\n if (_silentWavUrl) return _silentWavUrl;\n const sampleRate = 8000;\n const numSamples = sampleRate * 10;\n const fileSize = 44 + numSamples;\n const buffer = new ArrayBuffer(fileSize);\n const view = new DataView(buffer);\n\n const writeStr = (off: number, s: string) => {\n for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));\n };\n writeStr(0, 'RIFF');\n view.setUint32(4, fileSize - 8, true);\n writeStr(8, 'WAVE');\n writeStr(12, 'fmt ');\n view.setUint32(16, 16, true); // fmt chunk size\n view.setUint16(20, 1, true); // PCM\n view.setUint16(22, 1, true); // mono\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate, true); // byte rate\n view.setUint16(32, 1, true); // block align\n view.setUint16(34, 8, true); // 8 bits per sample\n writeStr(36, 'data');\n view.setUint32(40, numSamples, true);\n new Uint8Array(buffer, 44).fill(128); // 8-bit PCM silence = 128\n\n _silentWavUrl = URL.createObjectURL(new Blob([buffer], { type: 'audio/wav' }));\n return _silentWavUrl;\n}\n\nexport class Queue implements TrackQueueRef {\n private _tracks: Track[] = [];\n private readonly _actor;\n\n private readonly _onProgress?: (info: TrackInfo) => void;\n private readonly _onEnded?: () => void;\n private readonly _onPlayNextTrack?: (info: TrackInfo) => void;\n private readonly _onPlayPreviousTrack?: (info: TrackInfo) => void;\n private readonly _onStartNewTrack?: (info: TrackInfo) => void;\n private readonly _onError?: (error: Error) => void;\n private readonly _onPlayBlocked?: () => void;\n private readonly _onDebug?: (msg: string) => void;\n\n readonly playbackMethod: PlaybackMethod;\n\n private _volume: number;\n private _preloadNumTracks: number;\n private _playbackRate: number;\n\n /** Silent looping element that keeps the browser's MediaSession anchor alive. */\n private _mediaSessionAnchor: HTMLAudioElement | null = null;\n\n /** Index of the next track with a pre-scheduled gapless start, or null. */\n private _scheduledNextIndex: number | null = null;\n\n private _throttledUpdatePositionState = throttle(\n (duration: number, currentTime: number, playbackRate: number) =>\n updateMediaSessionPositionState(duration, currentTime, playbackRate),\n 1000,\n );\n\n constructor(options: GaplessOptions = {}) {\n const {\n tracks = [],\n onProgress,\n onEnded,\n onPlayNextTrack,\n onPlayPreviousTrack,\n onStartNewTrack,\n onError,\n onPlayBlocked,\n onDebug,\n playbackMethod = 'HYBRID',\n trackMetadata = [],\n volume: initialVolume = 1,\n preloadNumTracks = 2,\n playbackRate: initialPlaybackRate = 1,\n } = options;\n\n this._volume = Math.min(1, Math.max(0, initialVolume));\n this._preloadNumTracks = Math.max(0, preloadNumTracks);\n this._playbackRate = Math.min(4, Math.max(0.25, initialPlaybackRate));\n this.playbackMethod = playbackMethod;\n this._onProgress = onProgress;\n this._onEnded = onEnded;\n this._onPlayNextTrack = onPlayNextTrack;\n this._onPlayPreviousTrack = onPlayPreviousTrack;\n this._onStartNewTrack = onStartNewTrack;\n this._onError = onError;\n this._onPlayBlocked = onPlayBlocked;\n this._onDebug = onDebug;\n\n this._tracks = tracks.map(\n (url, i) =>\n new Track({\n trackUrl: url,\n index: i,\n queue: this,\n metadata: trackMetadata[i],\n })\n );\n\n // -----------------------------------------------------------------------\n // Wire up the queue machine with real action implementations.\n //\n // IMPORTANT: actions receive ({ context }) which reflects the in-progress\n // context (updated by prior assign() calls within the same transition).\n // Do NOT use this._actor.getSnapshot() inside actions — that returns the\n // pre-transition snapshot and won't reflect intermediate assign() updates.\n // -----------------------------------------------------------------------\n const machine = createQueueMachine({\n currentTrackIndex: 0,\n trackCount: this._tracks.length\n }).provide({\n actions: {\n deactivateCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n deactivateEndedTrack: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n activateAndPlayCurrent: ({ context }) => {\n const track = this._trackAt(context.currentTrackIndex);\n if (!track) return;\n track.activate();\n if (this._scheduledNextIndex !== track.index) {\n track.play();\n }\n },\n playOrContinueGapless: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (!cur) return;\n if (this._scheduledNextIndex !== cur.index) {\n cur.play();\n } else {\n this._scheduledNextIndex = null;\n this.onDebug(\n `onTrackEnded: gapless track ${cur.index} — sourceNode=${cur.hasSourceNode} isPlaying=${cur.isPlaying} machineState=${cur.machineState}`\n );\n cur.startProgressLoop();\n }\n },\n cancelAllGapless: () => this._cancelScheduledGapless(),\n notifyStartNewTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onStartNewTrack?.(cur.toInfo());\n },\n notifyPlayNextTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayNextTrack?.(cur.toInfo());\n },\n notifyPlayPreviousTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayPreviousTrack?.(cur.toInfo());\n },\n notifyEnded: () => this._onEnded?.(),\n updateMediaSessionMetadata: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) updateMediaSessionMetadata(cur.metadata);\n },\n preloadAhead: ({ context }) => {\n this._preloadAhead(context.currentTrackIndex);\n },\n playCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.play();\n },\n pauseCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.pause();\n },\n seekCurrent: ({ context, event }) => {\n const e = event as { type: 'SEEK'; time: number };\n this._trackAt(context.currentTrackIndex)?.seek(e.time);\n },\n seekCurrentToStartTime: ({ context, event }) => {\n const e = event as { type: 'GOTO'; startTime?: number };\n this._trackAt(context.currentTrackIndex)?.seek(e.startTime ?? 0);\n },\n scheduleGapless: ({ context }) => {\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n cancelScheduledGapless: () => {\n this._cancelScheduledGapless();\n },\n cancelAndRescheduleGapless: ({ context }) => {\n this._cancelScheduledGapless();\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n },\n });\n\n this._actor = createActor(machine);\n\n this._actor.subscribe((snapshot) => {\n const playing = snapshot.value === 'playing';\n updateMediaSessionPlaybackState(playing);\n if (playing) {\n this._startMediaSessionAnchor();\n } else {\n this._stopMediaSessionAnchor();\n }\n });\n\n this._actor.start();\n\n setupMediaSession({\n onPlay: () => this.play(),\n onPause: () => {\n if (this._actor.getSnapshot().value === 'playing') this.pause();\n },\n onNext: () => this.next(),\n onPrevious: () => this.previous(),\n onSeek: (t) => this.seek(t),\n });\n }\n\n // --------------------------------------------------------------------------\n // Public API\n // --------------------------------------------------------------------------\n\n play(): void {\n if (!this._currentTrack) return;\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n togglePlayPause(): void {\n if (this._actor.getSnapshot().value === 'playing') {\n this.pause();\n } else {\n this.play();\n }\n }\n\n next(): void {\n const snap = this._actor.getSnapshot();\n const nextIndex = snap.context.currentTrackIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n this._actor.send({ type: 'NEXT' });\n }\n\n previous(): void {\n const ct = this._currentTrack;\n if (ct && ct.currentTime > 8) {\n this._actor.send({ type: 'SEEK', time: 0 });\n return;\n }\n\n this._actor.send({ type: 'PREVIOUS' });\n }\n\n gotoTrack(index: number, playImmediately = false, startTime?: number): void {\n if (index < 0 || index >= this._tracks.length) return;\n this.onDebug(\n `gotoTrack(${index}, playImmediately=${playImmediately}, startTime=${startTime}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`\n );\n this._actor.send({ type: 'GOTO', index, playImmediately, startTime });\n }\n\n seek(time: number): void {\n this._actor.send({ type: 'SEEK', time });\n }\n\n setVolume(volume: number): void {\n const clamped = Math.min(1, Math.max(0, volume));\n this._volume = clamped;\n for (const track of this._tracks) track.setVolume(clamped);\n }\n\n setPlaybackRate(rate: number): void {\n const clamped = Math.min(4, Math.max(0.25, rate));\n this._playbackRate = clamped;\n this._currentTrack?.setPlaybackRate(clamped);\n this._cancelScheduledGapless();\n const snap = this._actor.getSnapshot();\n if (snap.value === 'playing') {\n this._tryScheduleGapless(snap.context.currentTrackIndex);\n }\n }\n\n addTrack(url: string, options: AddTrackOptions = {}): void {\n const index = this._tracks.length;\n const metadata = options.metadata ?? ({} as TrackMetadata);\n this._tracks.push(\n new Track({\n trackUrl: url,\n index,\n queue: this,\n skipHEAD: options.skipHEAD,\n metadata,\n })\n );\n this._actor.send({ type: 'ADD_TRACK' });\n }\n\n removeTrack(index: number): void {\n if (index < 0 || index >= this._tracks.length) return;\n this._tracks[index].destroy();\n this._tracks.splice(index, 1);\n for (let i = index; i < this._tracks.length; i++) {\n (this._tracks[i] as unknown as { index: number }).index = i;\n }\n if (this._scheduledNextIndex === index) {\n this._scheduledNextIndex = null;\n } else if (this._scheduledNextIndex !== null && this._scheduledNextIndex > index) {\n this._scheduledNextIndex--;\n }\n this._actor.send({ type: 'REMOVE_TRACK', index });\n }\n\n resumeAudioContext(): Promise<void> {\n return _resumeAudioContext();\n }\n\n destroy(): void {\n this._stopMediaSessionAnchor();\n this._mediaSessionAnchor = null;\n for (const track of this._tracks) track.destroy();\n this._tracks = [];\n this._actor.stop();\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTrack(): TrackInfo | undefined {\n return this._currentTrack?.toInfo();\n }\n\n get currentTrackIndex(): number {\n return this._actor.getSnapshot().context.currentTrackIndex;\n }\n\n get tracks(): readonly TrackInfo[] {\n return this._tracks.map((t) => t.toInfo());\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().value === 'playing';\n }\n\n get isPaused(): boolean {\n return this._actor.getSnapshot().value === 'paused';\n }\n\n get volume(): number {\n return this._volume;\n }\n\n get preloadNumTracks(): number {\n return this._preloadNumTracks;\n }\n\n set preloadNumTracks(value: number) {\n this._preloadNumTracks = Math.max(0, value);\n const snap = this._actor.getSnapshot();\n if (snap.value === 'playing') {\n this._preloadAhead(snap.context.currentTrackIndex);\n }\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n /** Snapshot of the queue state machine (state name + context). For debugging. */\n get queueSnapshot(): { state: string; context: { currentTrackIndex: number; trackCount: number } } {\n const snap = this._actor.getSnapshot();\n return { state: snap.value as string, context: snap.context };\n }\n\n // --------------------------------------------------------------------------\n // TrackQueueRef — called by Track instances\n // --------------------------------------------------------------------------\n\n onTrackEnded(track: Track): void {\n const snap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded track=${track.index} queueState=${snap.value} curIdx=${snap.context.currentTrackIndex}`\n );\n // Compare object identity, not just index — after removeTrack(), a\n // different track may occupy the same index as the ended track.\n if (track !== this._trackAt(snap.context.currentTrackIndex)) return;\n\n this._actor.send({ type: 'TRACK_ENDED' });\n const newSnap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded after TRACK_ENDED → queueState=${newSnap.value} curIdx=${newSnap.context.currentTrackIndex}`\n );\n }\n\n onTrackBufferReady(track: Track): void {\n this._actor.send({ type: 'TRACK_LOADED', index: track.index });\n }\n\n onProgress(info: TrackInfo): void {\n if (info.index !== this._actor.getSnapshot().context.currentTrackIndex) return;\n if (!isNaN(info.duration)) {\n this._throttledUpdatePositionState(info.duration, info.currentTime, this._playbackRate);\n }\n this._onProgress?.(info);\n }\n\n onError(error: Error): void {\n this._onError?.(error);\n }\n\n onPlayBlocked(): void {\n this._actor.send({ type: 'PAUSE' });\n this._onPlayBlocked?.();\n }\n\n onPreloadReady(track: Track): void {\n const snap = this._actor.getSnapshot();\n if (track.index !== snap.context.currentTrackIndex) return;\n this._preloadAhead(snap.context.currentTrackIndex);\n }\n\n onDebug(msg: string): void {\n this._onDebug?.(msg);\n }\n\n // --------------------------------------------------------------------------\n // Private helpers\n // --------------------------------------------------------------------------\n\n /** Look up a track by index — safe for use inside machine actions. */\n private _trackAt(index: number): Track | undefined {\n return this._tracks[index];\n }\n\n private get _currentTrack(): Track | undefined {\n return this._tracks[this._actor.getSnapshot().context.currentTrackIndex];\n }\n\n private _preloadAhead(fromIndex: number): void {\n const cur = this._trackAt(fromIndex);\n\n // Bandwidth-contention gate: while the current track is playing via HTML5\n // and its own Web Audio buffer is still being fetched+decoded, holding\n // off on every next-track preload prevents two concurrent large MP3\n // downloads from delaying the current track's crossover (and therefore\n // the moment from which all future gapless transitions are\n // sample-accurate). When the current track's BUFFER_READY fires,\n // notifyBufferReady → TRACK_LOADED → preloadAhead re-runs, this gate\n // is no longer engaged, and next-track fetches proceed sequentially\n // as buffers complete.\n //\n // This lives here (not in the queue machine) because preloadAhead is\n // invoked as a transition action from ~7 different events, and the\n // gate's data source — the current track's machine state — isn't\n // owned by the queue machine. Pushing the check into every caller\n // would just duplicate the same read.\n if (\n cur != null &&\n cur.isPlaying &&\n cur.playbackType === 'HTML5' &&\n cur.webAudioLoadingState === 'LOADING'\n ) {\n this.onDebug(`_preloadAhead(${fromIndex}): deferring all — current track buffer still loading`);\n return;\n }\n\n // Speculative-load throttle: the immediate next track is always allowed\n // (it must be ready for gapless scheduling), but tracks beyond that are\n // deferred until the current track has played past min(duration*0.2, 15s)\n // — the point at which we have evidence the user is committing to\n // listening, not skipping.\n const curBelowThreshold =\n cur != null &&\n cur.isPlaying &&\n (() => {\n const threshold = isNaN(cur.duration) ? 15 : Math.min(cur.duration * 0.2, 15);\n return cur.currentTime < threshold;\n })();\n\n const limit = fromIndex + this._preloadNumTracks + 1;\n this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length} belowThreshold=${curBelowThreshold}`);\n for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {\n const t = this._tracks[i];\n if (i > fromIndex + 1 && curBelowThreshold) {\n this.onDebug(`_preloadAhead: deferring track ${i} (current at ${cur!.currentTime.toFixed(1)}s, below threshold)`);\n return;\n }\n if (!t.isBufferLoaded) {\n this.onDebug(`_preloadAhead: starting preload for track ${i}`);\n t.preload();\n return;\n } else {\n this.onDebug(`_preloadAhead: track ${i} already loaded`);\n }\n }\n }\n\n private _cancelScheduledGapless(): void {\n if (this._scheduledNextIndex === null) return;\n const track = this._trackAt(this._scheduledNextIndex);\n if (track) {\n track.cancelGaplessStart();\n this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`);\n }\n // Cancel any HTML5 gain mute that was scheduled on the current track,\n // but only if it's still in HTML5 state. After crossover the gain node\n // is managed by the crossfade ramp — cancelScheduledValues would wipe it.\n const cur = this._trackAt(this._actor.getSnapshot().context.currentTrackIndex);\n if (cur && cur.playbackType === 'HTML5') cur.cancelHtml5Mute();\n this._scheduledNextIndex = null;\n }\n\n private _tryScheduleGapless(curIndex: number): void {\n const ctx = getAudioContext();\n if (!ctx || this.playbackMethod === 'HTML5_ONLY') return;\n\n const nextIndex = curIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n const current = this._tracks[curIndex];\n const next = this._tracks[nextIndex];\n\n if (\n !current.isBufferLoaded ||\n !next.isBufferLoaded ||\n this._scheduledNextIndex === nextIndex ||\n !current.isPlaying\n )\n return;\n\n const endTime = this._computeTrackEndTime(current);\n if (endTime === null) return;\n\n if (endTime < ctx.currentTime + 0.01) return;\n\n const timeUntilEnd = endTime - ctx.currentTime;\n if (current.playbackType === 'HTML5' && timeUntilEnd > MAX_SCHEDULE_LOOKAHEAD) {\n this.onDebug(\n `_tryScheduleGapless: deferring — HTML5 track ${curIndex} has ${timeUntilEnd.toFixed(1)}s remaining (max lookahead=${MAX_SCHEDULE_LOOKAHEAD}s)`\n );\n return;\n }\n\n if (!next.scheduleGaplessStart(endTime)) {\n this.onDebug(\n `_tryScheduleGapless: track ${nextIndex} rejected SCHEDULE_GAPLESS (state=${next.machineState})`\n );\n return;\n }\n // When scheduling from an HTML5-clock prediction, mute the HTML5 gain\n // at endTime so a stalled/late HTML5 element is silenced rather than\n // overlapping the next track.\n if (current.playbackType === 'HTML5') {\n current.scheduleHtml5Mute(endTime);\n }\n this.onDebug(\n `_tryScheduleGapless: scheduled track ${nextIndex} at endTime=${endTime.toFixed(3)} (in ${(endTime - ctx.currentTime).toFixed(1)}s) curPlaybackType=${current.playbackType}`\n );\n this._scheduledNextIndex = nextIndex;\n }\n\n private _computeTrackEndTime(track: Track): number | null {\n const ctx = getAudioContext();\n if (!ctx || !track.isBufferLoaded) return null;\n\n // Web Audio playback: derive the end time from the source node's live\n // playback anchor. For an uninterrupted gapless chain this equals\n // scheduledStart + duration exactly, but unlike that formula it remains\n // correct after a pause/resume re-anchors the source at a later context\n // time. (Using the stale scheduled start here is how the next track got\n // scheduled mid-track, with both tracks audible simultaneously.)\n const anchoredEnd = track.playbackEndContextTime;\n if (anchoredEnd !== null) {\n return anchoredEnd > ctx.currentTime ? anchoredEnd : null;\n }\n\n const duration = track.duration;\n if (isNaN(duration)) return null;\n const remaining = (duration - track.currentTime) / this._playbackRate;\n if (remaining <= 0) return null;\n return ctx.currentTime + remaining;\n }\n\n private _startMediaSessionAnchor(): void {\n if (typeof Audio === 'undefined') return;\n if (!this._mediaSessionAnchor) {\n this._mediaSessionAnchor = new Audio(getSilentWavUrl());\n this._mediaSessionAnchor.loop = true;\n // Volume stays at default (1) — Chrome/Safari may ignore a zero-volume\n // element for media focus. The WAV content is already silence.\n }\n if (this._mediaSessionAnchor.paused) {\n this._mediaSessionAnchor.play().catch(() => {});\n }\n }\n\n private _stopMediaSessionAnchor(): void {\n if (this._mediaSessionAnchor && !this._mediaSessionAnchor.paused) {\n this._mediaSessionAnchor.pause();\n }\n }\n}\n","// ---------------------------------------------------------------------------\n// Singleton AudioContext manager\n//\n// A single AudioContext is shared across all Track instances. This is\n// essential: all AudioBufferSourceNodes must share the same context so that\n// audioContext.currentTime is a common monotonic clock, enabling\n// sample-accurate scheduling of back-to-back tracks.\n// ---------------------------------------------------------------------------\n\nconst isBrowser = typeof window !== 'undefined';\n\nlet _audioContext: AudioContext | null | undefined = undefined;\n\nfunction createAudioContext(): AudioContext | null {\n if (!isBrowser) return null;\n const Ctor =\n (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })\n .AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctor) return null;\n return new Ctor();\n}\n\n/**\n * Return the shared AudioContext if it has already been created, otherwise null.\n *\n * Does NOT create the context — call resumeAudioContext() from a user-gesture\n * handler to create and resume it. This prevents the browser warning:\n * \"An AudioContext was prevented from starting automatically.\"\n */\nexport function getAudioContext(): AudioContext | null {\n // undefined = never initialised (return null without creating)\n // null = explicitly disabled or unavailable\n // AudioContext = ready to use\n if (_audioContext === undefined) return null;\n return _audioContext;\n}\n\n/**\n * Create (if needed) and resume the AudioContext.\n * Must be called from a user-gesture handler (click, keydown, etc.).\n * Safe to call multiple times — subsequent calls are cheap no-ops.\n */\nexport function resumeAudioContext(): Promise<void> {\n if (_audioContext === undefined) {\n _audioContext = createAudioContext();\n }\n const ctx = _audioContext;\n if (ctx && ctx.state === 'suspended') {\n return ctx.resume();\n }\n return Promise.resolve();\n}\n\n/**\n * Reset the cached AudioContext — used in tests so each test gets a fresh mock.\n * @internal\n */\nexport function _resetAudioContext(): void {\n _audioContext = undefined;\n}\n\n/**\n * Directly set the cached AudioContext — used in tests to inject a mock instance.\n * @internal\n */\nexport function _setAudioContext(ctx: AudioContext | null): void {\n _audioContext = ctx;\n}\n","// ---------------------------------------------------------------------------\n// Media Session API integration\n//\n// Provides OS-level media controls (lock screen, notification shade, keyboard\n// media keys). Gracefully no-ops when the API is unavailable.\n// ---------------------------------------------------------------------------\n\nimport type { TrackMetadata } from '../types';\n\nconst hasMediaSession =\n typeof navigator !== 'undefined' && 'mediaSession' in navigator;\n\nexport interface MediaSessionHandlers {\n onPlay: () => void;\n onPause: () => void;\n onNext: () => void;\n onPrevious: () => void;\n onSeek: (time: number) => void;\n}\n\n/** Register Media Session action handlers. Call once after construction. */\nexport function setupMediaSession(handlers: MediaSessionHandlers): void {\n if (!hasMediaSession) return;\n const { mediaSession } = navigator;\n mediaSession.setActionHandler('play', handlers.onPlay);\n mediaSession.setActionHandler('pause', handlers.onPause);\n mediaSession.setActionHandler('nexttrack', handlers.onNext);\n mediaSession.setActionHandler('previoustrack', handlers.onPrevious);\n mediaSession.setActionHandler('seekto', (details) => {\n if (details.seekTime != null) handlers.onSeek(details.seekTime);\n });\n}\n\n/** Update the OS metadata display (title, artist, album, artwork). */\nexport function updateMediaSessionMetadata(metadata?: TrackMetadata): void {\n if (!hasMediaSession) return;\n if (!metadata) {\n navigator.mediaSession.metadata = null;\n return;\n }\n navigator.mediaSession.metadata = new MediaMetadata({\n title: metadata.title ?? '',\n artist: metadata.artist ?? '',\n album: metadata.album ?? '',\n artwork: metadata.artwork ?? [],\n });\n}\n\n/** Sync the OS playback state indicator (playing / paused). */\nexport function updateMediaSessionPlaybackState(\n isPlaying: boolean,\n): void {\n if (!hasMediaSession) return;\n navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';\n}\n\n/** Update the OS position/duration display (lock screen scrubber). */\nexport function updateMediaSessionPositionState(\n duration: number,\n position: number,\n playbackRate = 1,\n): void {\n if (!hasMediaSession) return;\n try {\n navigator.mediaSession.setPositionState({ duration, position, playbackRate });\n } catch {\n // Some browsers throw on invalid values (e.g. NaN duration)\n }\n}\n","// ---------------------------------------------------------------------------\n// QueueMachine — top-level queue state (xstate v5)\n//\n// States:\n// idle No tracks, or not yet started.\n// playing A track is actively playing.\n// paused Explicitly paused by the user.\n// ended The last track in the queue has finished.\n//\n// Root-level `on:` eliminates the handler duplication that plagued v2.\n//\n// Named actions (no-op defaults here, real implementations via .provide()):\n// Actions before assign() see the OLD context (e.g. deactivateCurrent\n// sees the old currentTrackIndex). Actions after assign() see the NEW\n// context (e.g. activateAndPlayCurrent sees the incremented index).\n// Implementations MUST use the ({ context }) parameter, NOT getSnapshot().\n// ---------------------------------------------------------------------------\n\nimport { setup, assign } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface QueueContext {\n currentTrackIndex: number;\n trackCount: number;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type QueueEvent =\n | { type: 'PLAY' }\n | { type: 'PAUSE' }\n | { type: 'TOGGLE' }\n | { type: 'NEXT' }\n | { type: 'PREVIOUS' }\n | { type: 'GOTO'; index: number; playImmediately?: boolean; startTime?: number }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'ADD_TRACK' }\n | { type: 'REMOVE_TRACK'; index: number }\n | { type: 'TRACK_ENDED' }\n | { type: 'TRACK_LOADED'; index: number };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createQueueMachine(initialContext: QueueContext) {\n return setup({\n types: {\n context: {} as QueueContext,\n events: {} as QueueEvent,\n },\n guards: {\n hasNextTrack: ({ context }) => context.currentTrackIndex + 1 < context.trackCount,\n playImmediately: ({ event }) =>\n !!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,\n willBeEmpty: ({ context }) => context.trackCount <= 1,\n isRemovingCurrent: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n return e.index === context.currentTrackIndex;\n },\n },\n actions: {\n // --- named assign actions (avoids mixing inline assign with custom actions) ---\n incrementTrackCount: assign({\n trackCount: ({ context }) => context.trackCount + 1,\n }),\n decrementTrackCount: assign({\n trackCount: ({ context }) => Math.max(0, context.trackCount - 1),\n currentTrackIndex: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n const newCount = Math.max(0, context.trackCount - 1);\n if (e.index < context.currentTrackIndex) {\n return Math.max(0, context.currentTrackIndex - 1);\n }\n // Clamp to last valid index when removing at or after current\n return newCount > 0\n ? Math.min(context.currentTrackIndex, newCount - 1)\n : 0;\n },\n }),\n gotoTrackIndex: assign({\n currentTrackIndex: ({ event }) =>\n (event as { type: 'GOTO'; index: number }).index,\n }),\n advanceToNextTrack: assign({\n currentTrackIndex: ({ context }) => {\n const next = context.currentTrackIndex + 1;\n return next < context.trackCount ? next : context.currentTrackIndex;\n },\n }),\n goToPreviousTrack: assign({\n currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),\n }),\n advanceOnTrackEnd: assign({\n currentTrackIndex: ({ context }) =>\n Math.min(context.currentTrackIndex + 1, context.trackCount - 1),\n }),\n resetToFirstTrack: assign({\n currentTrackIndex: () => 0,\n }),\n // --- side-effect actions (no-op defaults, provided by Queue.ts) ---\n deactivateCurrent: () => {},\n deactivateEndedTrack: () => {},\n activateAndPlayCurrent: () => {},\n playOrContinueGapless: () => {},\n cancelAllGapless: () => {},\n notifyStartNewTrack: () => {},\n notifyPlayNextTrack: () => {},\n notifyPlayPreviousTrack: () => {},\n notifyEnded: () => {},\n updateMediaSessionMetadata: () => {},\n preloadAhead: () => {},\n playCurrent: () => {},\n pauseCurrent: () => {},\n seekCurrent: () => {},\n seekCurrentToStartTime: () => {},\n scheduleGapless: () => {},\n cancelScheduledGapless: () => {},\n cancelAndRescheduleGapless: () => {},\n },\n }).createMachine({\n id: 'queue',\n initial: 'idle',\n context: initialContext,\n\n // Global handlers — shared across all states\n on: {\n ADD_TRACK: {\n actions: 'incrementTrackCount',\n },\n REMOVE_TRACK: {\n actions: 'decrementTrackCount',\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle\n // -----------------------------------------------------------------\n idle: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToStartTime',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToStartTime',\n 'preloadAhead',\n ],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // playing\n // -----------------------------------------------------------------\n playing: {\n on: {\n REMOVE_TRACK: [\n {\n guard: 'willBeEmpty',\n target: 'idle',\n actions: ['cancelAllGapless', 'decrementTrackCount'],\n },\n {\n guard: 'isRemovingCurrent',\n actions: [\n 'cancelAllGapless',\n 'decrementTrackCount',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: ['decrementTrackCount', 'cancelAndRescheduleGapless', 'preloadAhead'],\n },\n ],\n PAUSE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n TOGGLE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToStartTime',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToStartTime',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent', 'cancelAndRescheduleGapless'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'playing',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'playOrContinueGapless',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: [\n {\n // When the *current* track's buffer becomes ready while we are\n // already playing, that's a mid-stream HTML5 → Web Audio\n // crossover. Any existing gapless schedule was based on the old\n // HTML5-clock end-time prediction; cancel and re-schedule using\n // the now-authoritative WebAudio end time.\n guard: ({ context, event }) => event.index === context.currentTrackIndex,\n actions: ['cancelAndRescheduleGapless', 'preloadAhead'],\n },\n {\n actions: ['scheduleGapless', 'preloadAhead'],\n },\n ],\n },\n },\n\n // -----------------------------------------------------------------\n // paused\n // -----------------------------------------------------------------\n paused: {\n on: {\n REMOVE_TRACK: [\n {\n guard: 'willBeEmpty',\n target: 'idle',\n actions: ['cancelAllGapless', 'decrementTrackCount'],\n },\n {\n guard: 'isRemovingCurrent',\n actions: ['cancelAllGapless', 'decrementTrackCount', 'preloadAhead'],\n },\n {\n actions: ['decrementTrackCount', 'preloadAhead'],\n },\n ],\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n TOGGLE: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToStartTime',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToStartTime',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'paused',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // ended\n // -----------------------------------------------------------------\n ended: {\n on: {\n ADD_TRACK: {\n target: 'paused',\n actions: 'incrementTrackCount',\n },\n PLAY: {\n target: 'playing',\n actions: [\n 'resetToFirstTrack',\n 'playCurrent',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n 'scheduleGapless',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToStartTime',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToStartTime',\n 'preloadAhead',\n ],\n },\n ],\n },\n },\n },\n });\n}\n\nexport type QueueMachine = ReturnType<typeof createQueueMachine>;\n","// ---------------------------------------------------------------------------\n// Track — owns one audio track's Web Audio nodes and drives TrackMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor, fromPromise } from 'xstate';\nimport { getAudioContext, resumeAudioContext } from './utils/audioContext';\nimport { createTrackMachine } from './machines/track.machine';\nimport { fetchDecodeMachine } from './machines/fetchDecode.machine';\nimport type { TrackContext } from './machines/track.machine';\nimport type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType, PlaybackMethod } from './types';\n\nexport interface TrackQueueRef {\n onTrackEnded(track: Track): void;\n onTrackBufferReady(track: Track): void;\n onPreloadReady(track: Track): void;\n onProgress(info: TrackInfo): void;\n onError(error: Error): void;\n onPlayBlocked(): void;\n onDebug(msg: string): void;\n readonly volume: number;\n readonly playbackMethod: PlaybackMethod;\n readonly playbackRate: number;\n readonly currentTrackIndex: number;\n}\n\n/** How close to the end (in seconds) before we attempt gapless scheduling. */\nconst GAPLESS_SCHEDULE_LOOKAHEAD = 5;\n\n/** How many seconds into HTML5 playback before we preload the next track. */\nconst PRELOAD_DELAY = 15;\n\n/**\n * Crossfade duration (seconds) for the mid-stream HTML5 → Web Audio handoff.\n * Both sides of the crossfade run on AudioContext-clock gain ramps\n * (the HTML5 side via a GainNode after MediaElementAudioSourceNode), so\n * the join is sample-accurate. 30 ms is short enough to be inaudible as a\n * fade and long enough to mask any sample-level discontinuity between the\n * two streams.\n */\nconst CROSSOVER_FADE_SEC = 0.03;\n\n\nexport class Track {\n readonly index: number;\n readonly metadata: TrackMetadata;\n\n private _trackUrl: string;\n private _resolvedUrl: string;\n private readonly skipHEAD: boolean;\n /** Temporary holder between fetch and decode steps (unserializable — stays on Track class). */\n private _pendingArrayBuffer: ArrayBuffer | null = null;\n\n // ---- HTML5 Audio ---------------------------------------------------------\n readonly audio: HTMLAudioElement;\n\n // ---- Web Audio nodes -----------------------------------------------------\n private readonly _playbackMethod: PlaybackMethod;\n\n private get ctx(): AudioContext | null {\n if (this._playbackMethod === 'HTML5_ONLY') return null;\n const context = getAudioContext();\n if (context && !this.gainNode) {\n this.gainNode = context.createGain();\n this.gainNode.gain.value = this.audio.volume;\n this.gainNode.connect(context.destination);\n\n // Route the HTML5 element through the AudioContext via a\n // MediaElementAudioSourceNode + dedicated GainNode. With this routing,\n // the HTML5 side has its own AudioContext-clock gain we can ramp\n // sample-accurately during crossover, eliminating the click that an\n // abrupt audio.pause() leaves at the cut point. Volume control moves\n // entirely onto gainNode (master); we set audio.volume = 1 so the\n // browser-side and AudioContext-side gains don't multiply.\n //\n // createMediaElementSource severs the element's default audio output\n // for the rest of its life, so once this runs, all HTML5 playback\n // flows through the AudioContext. CORS: cross-origin audio without\n // Access-Control-Allow-Origin will produce silence through the\n // MediaElementAudioSourceNode path. If the call throws (rare; some\n // legacy implementations or double-attach), fall back to the\n // pre-routing behavior with audio.volume controlling HTML5 directly.\n try {\n this._mediaElementSource = context.createMediaElementSource(this.audio);\n this._html5GainNode = context.createGain();\n this._html5GainNode.gain.value = 1;\n this._mediaElementSource.connect(this._html5GainNode);\n this._html5GainNode.connect(this.gainNode);\n this.audio.volume = 1;\n } catch {\n this._mediaElementSource = null;\n this._html5GainNode = null;\n }\n }\n return context;\n }\n\n private gainNode: GainNode | null = null;\n private _mediaElementSource: MediaElementAudioSourceNode | null = null;\n private _html5GainNode: GainNode | null = null;\n private sourceNode: AudioBufferSourceNode | null = null;\n audioBuffer: AudioBuffer | null = null;\n /** AudioContext.currentTime at the start of the current playback segment. */\n /**\n * Offset (in seconds) between the decoded buffer's \"offset 0\" and the music's\n * \"offset 0\". Some MP3 files include ID3v2 metadata, encoder priming samples,\n * or container padding at the start; HTML5 audio elements skip past these\n * natively (audio.currentTime=0 means music start), but `decodeAudioData` in\n * some browsers includes them in the decoded buffer (buffer offset 0 = file\n * start, music actually starts at offset _bufferStartPaddingSec).\n *\n * Without this shift, calling source.start(when, audio.currentTime) plays\n * `audio.currentTime` seconds AHEAD of what HTML5 was just outputting,\n * sounding like a backward skip at crossover. We compute this once both\n * the buffer and audio.duration are known, and apply it as\n * source.start(when, trackTime + _bufferStartPaddingSec)\n * everywhere we read from the buffer. User-facing time (currentTime/duration\n * getters) continues to be reported in music-time, not buffer-time.\n */\n private _bufferStartPaddingSec = 0;\n private _bufferAlignmentMeasured = false;\n\n private _waRefCtxTime = 0;\n /** Track position (seconds) at the start of the current playback segment. */\n private _waRefTrackTime = 0;\n /** Track-time (seconds) frozen at the moment of the most recent pause. */\n private pausedAtTrackTime = 0;\n // ---- FSM -----------------------------------------------------------------\n private readonly _actor;\n\n // ---- Callbacks -----------------------------------------------------------\n private readonly queueRef: TrackQueueRef;\n private rafId: number | null = null;\n private _notifiedPreloadThreshold = false;\n\n constructor(opts: {\n trackUrl: string;\n index: number;\n queue: TrackQueueRef;\n skipHEAD?: boolean;\n metadata?: TrackMetadata;\n }) {\n this.index = opts.index;\n this._trackUrl = opts.trackUrl;\n this._resolvedUrl = opts.trackUrl;\n this.skipHEAD = opts.skipHEAD ?? false;\n this.metadata = opts.metadata ?? {};\n this.queueRef = opts.queue;\n\n // HTML5 Audio\n this.audio = new Audio();\n this.audio.preload = 'none';\n // crossOrigin must be set BEFORE src for MediaElementAudioSourceNode to\n // actually expose the element's audio to the AudioContext graph. Without\n // it, even servers that DO send CORS headers result in the\n // MediaElementSource being treated as cross-origin tainted, and the node\n // outputs silence — manifesting as \"HTML5 plays fine until WebAudio\n // takes over, then suddenly audible\". HTML5_ONLY mode skips this since\n // it doesn't route through AudioContext; setting crossOrigin there\n // would make non-CORS sources fail to load at all.\n if (opts.queue.playbackMethod !== 'HTML5_ONLY') {\n this.audio.crossOrigin = 'anonymous';\n }\n this.audio.src = this._trackUrl;\n this.audio.volume = opts.queue.volume;\n this.audio.controls = false;\n this.audio.onerror = () => {\n const code = this.audio.error?.code;\n if (code === 1) return;\n const msg = this.audio.error?.message ?? 'unknown';\n this.queueRef.onError(\n new Error(`HTML5 audio error on track ${this.index} (code ${code}): ${msg}`)\n );\n };\n this.audio.onended = () => {\n this.queueRef.onDebug(\n `audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n this._actor.send({ type: 'HTML5_ENDED' });\n };\n\n this._playbackMethod = opts.queue.playbackMethod;\n\n const initialContext: TrackContext = {\n trackUrl: this._trackUrl,\n resolvedUrl: this._trackUrl,\n skipHEAD: this.skipHEAD,\n playbackType: 'HTML5',\n webAudioLoadingState: 'NONE',\n isPlaying: false,\n scheduledStartContextTime: null,\n notifiedLookahead: false,\n fetchStarted: false,\n pendingPlay: false,\n };\n const machine = createTrackMachine(initialContext).provide({\n guards: {\n canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),\n isWebAudioOnly: () => this._playbackMethod === 'WEBAUDIO_ONLY',\n },\n actors: {\n fetchDecode: fetchDecodeMachine.provide({\n actors: {\n resolveUrl: fromPromise(async ({ signal }) => {\n const res = await fetch(this._trackUrl, { method: 'HEAD', signal });\n if (res.redirected && res.url) {\n // Cache the resolved URL so the WebAudio GET below uses it\n // directly (avoiding a second redirect round-trip). Do NOT\n // overwrite this.audio.src — the HTML5 element may currently\n // be streaming, and assigning a new src aborts playback,\n // resets audio.currentTime to 0, and reloads from the new\n // URL. The browser already handles the original URL's\n // redirect transparently for HTML5; updating src here causes\n // a perceived jump back to the start mid-playback (which the\n // user then notices later as a \"skip\" at crossover, since\n // audio.currentTime captured at crossover time reflects the\n // post-reset position rather than the user's actual progress).\n this._resolvedUrl = res.url;\n return res.url;\n }\n return null;\n }),\n fetchAudio: fromPromise(async ({ input, signal }) => {\n const { resolvedUrl } = input as { resolvedUrl: string };\n const res = await fetch(resolvedUrl, { signal });\n if (!res.ok) throw new Error(`HTTP ${res.status} for ${resolvedUrl}`);\n this._pendingArrayBuffer = await res.arrayBuffer();\n }),\n decodeAudio: fromPromise(async () => {\n const buf = this._pendingArrayBuffer;\n this._pendingArrayBuffer = null;\n if (!buf || !this.ctx) throw new Error('No ArrayBuffer or AudioContext');\n this.audioBuffer = await this.ctx.decodeAudioData(buf);\n this._maybeComputeBufferAlignment();\n // NOTE: do NOT notify the queue from here. The fetchDecode child\n // is about to sendParent('BUFFER_READY'), which the track machine\n // handles by transitioning state (and, in html5, by performing\n // the mid-stream crossover). If we notified the queue first, it\n // would observe a stale playbackType ('HTML5') and incorrectly\n // defer next-track preload via the HTML5 deferral guard. The\n // notification is emitted from the BUFFER_READY transitions in\n // track.machine, after the crossover/state update is in effect.\n }),\n },\n }),\n },\n actions: {\n triggerFetchForPendingPlay: () => {\n this.preload();\n resumeAudioContext();\n },\n playHtml5: () => this._playHtml5(),\n startSourceNode: () => {\n this._startSourceNode(this.pausedAtTrackTime);\n },\n crossoverHtml5ToWebAudio: ({ context }: { context: TrackContext }) => {\n this._crossoverHtml5ToWebAudio(context.isPlaying);\n },\n startScheduledSourceNode: ({ context }: { context: TrackContext }) => {\n const when = context.scheduledStartContextTime;\n if (when === null || !this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._maybeComputeBufferAlignment();\n this._stopSourceNode();\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.playbackRate.value = this.queueRef.playbackRate;\n this.sourceNode.connect(this.gainNode);\n // gainNode → destination connected once at ctx setup; do not re-connect\n // here or the gainNode's output is duplicated (Web Audio sums multiple\n // edges between the same pair of nodes).\n this.sourceNode.onended = this._handleWebAudioEnded;\n // Skip past any decoded-buffer start padding (ID3 tags, encoder\n // priming) so the gapless transition begins at music-time 0, not at\n // half a second of silence/garbage. _bufferStartPaddingSec is\n // computed at decode time via _ensureBufferAlignment.\n this.sourceNode.start(when, this._bufferStartPaddingSec);\n const bufRemaining = this.audioBuffer.duration - this._bufferStartPaddingSec;\n const schedRate = this.sourceNode.playbackRate.value || 1;\n if (bufRemaining > 0) {\n this.sourceNode.stop(when + bufRemaining / schedRate);\n }\n this._waRefCtxTime = when;\n this._waRefTrackTime = 0;\n this.queueRef.onDebug(\n `startScheduledSourceNode track=${this.index} when=${when.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(when - this.ctx.currentTime).toFixed(3)}s padding=${this._bufferStartPaddingSec.toFixed(3)}s`\n );\n },\n startProgressLoop: () => this.startProgressLoop(),\n pauseHtml5: () => this.audio.pause(),\n freezePausedTime: () => {\n const t = this.currentTime;\n this.pausedAtTrackTime = isFinite(t) ? t : 0;\n },\n stopSourceNode: () => this._stopSourceNode(),\n stopProgressLoop: () => this._stopProgressLoop(),\n reportProgress: () => {\n queueMicrotask(() => this.queueRef.onProgress(this.toInfo()));\n },\n seekHtml5: () => this._seekHtml5(),\n seekWebAudio: () => this._seekWebAudio(),\n resetHtml5Element: () => {\n this.audio.currentTime = 0;\n },\n resetTiming: () => {\n this._waRefCtxTime = 0;\n this._waRefTrackTime = 0;\n this.pausedAtTrackTime = 0;\n },\n notifyTrackEnded: () => {\n queueMicrotask(() => this.queueRef.onTrackEnded(this));\n },\n notifyBufferReady: () => {\n // Emitted from BUFFER_READY transitions so the queue observes the\n // post-transition state (e.g. WEBAUDIO after a crossover) rather\n // than the pre-transition state.\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n },\n },\n });\n this._actor = createActor(machine);\n this._actor.start();\n }\n\n // --------------------------------------------------------------------------\n // Public playback controls\n // --------------------------------------------------------------------------\n\n play(): void {\n this.queueRef.onDebug(\n `Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`\n );\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n seek(time: number): void {\n if (!isFinite(time)) return;\n const clamped = Math.max(0, isNaN(this.duration) ? time : Math.min(time, this.duration));\n this.pausedAtTrackTime = clamped;\n this._actor.send({ type: 'SEEK', time: clamped });\n }\n\n setVolume(v: number): void {\n const vol = Math.min(1, Math.max(0, v));\n // When the HTML5 element is routed through MediaElementAudioSourceNode,\n // master volume lives entirely on gainNode — keep audio.volume at 1 so the\n // browser-side and AudioContext-side gains don't multiply (giving v²).\n // Otherwise (HTML5_ONLY mode, or pre-ctx), audio.volume is the master.\n if (this._mediaElementSource) {\n this.audio.volume = 1;\n } else {\n this.audio.volume = vol;\n }\n if (this.gainNode) this.gainNode.gain.value = vol;\n this._actor.send({ type: 'SET_VOLUME', volume: vol });\n }\n\n setPlaybackRate(rate: number): void {\n // Freeze current track position at the old rate before switching.\n // Skip the anchor rewrite during the scheduling lead (when the source\n // is scheduled but ctx.currentTime hasn't reached _waRefCtxTime yet) —\n // the source hasn't started producing audio, so the anchors are still\n // correct for the future start. Rewriting them with a negative elapsed\n // time would corrupt playbackEndContextTime and cause overlap/gap.\n if (this.ctx && this.sourceNode && this._actor.getSnapshot().context.isPlaying\n && this.ctx.currentTime >= this._waRefCtxTime) {\n const oldRate = this.sourceNode.playbackRate.value;\n this._waRefTrackTime = this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * oldRate;\n this._waRefCtxTime = this.ctx.currentTime;\n }\n this.audio.playbackRate = rate;\n if (this.sourceNode) {\n this.sourceNode.playbackRate.value = rate;\n // Re-issue the hard stop ceiling at the new end time. The spec\n // allows repeated stop() calls (latest wins), so this overwrites\n // the stale stop scheduled at the old rate.\n if (this.audioBuffer && this.ctx) {\n const remaining = this.audioBuffer.duration - this._bufferStartPaddingSec - this._waRefTrackTime;\n if (remaining > 0) {\n try {\n this.sourceNode.stop(this._waRefCtxTime + remaining / rate);\n } catch { /* already stopped */ }\n }\n }\n }\n }\n\n preload(): void {\n this.queueRef.onDebug(\n `preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`\n );\n if (this._actor.getSnapshot().value === 'idle') {\n this._actor.send({ type: 'PRELOAD' });\n }\n if (this.audioBuffer) return;\n resumeAudioContext();\n if (!this.ctx) return;\n this._actor.send({ type: 'START_FETCH' });\n }\n\n seekToEnd(secondsFromEnd = 6): void {\n const dur = this.duration;\n if (!isNaN(dur) && dur > secondsFromEnd) {\n this.seek(dur - secondsFromEnd);\n }\n }\n\n activate(): void {\n this._actor.send({ type: 'ACTIVATE' });\n }\n\n deactivate(): void {\n this.queueRef.onDebug(\n `Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`\n );\n this._notifiedPreloadThreshold = false;\n this._actor.send({ type: 'DEACTIVATE' });\n this.queueRef.onDebug(\n `Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`\n );\n }\n\n destroy(): void {\n this.deactivate();\n this._pendingArrayBuffer = null;\n this.audioBuffer = null;\n this.gainNode?.disconnect();\n this.gainNode = null;\n this._actor.stop(); // Stops spawned fetchDecode child actor, aborting in-flight fetches\n }\n\n // --------------------------------------------------------------------------\n // Gapless scheduling (called by Queue)\n // --------------------------------------------------------------------------\n\n /** Schedule the HTML5 gain node to mute at `when` — safety valve so an\n * HTML5 element that runs past the predicted end is silenced instead of\n * overlapping the next track. No-op if the HTML5 gain path isn't active. */\n scheduleHtml5Mute(when: number): void {\n if (!this._html5GainNode || !this.ctx) return;\n this._html5GainNode.gain.setValueAtTime(1, when - 0.005);\n this._html5GainNode.gain.linearRampToValueAtTime(0, when);\n }\n\n /** Cancel any scheduled HTML5 gain mute (called when gapless is cancelled). */\n cancelHtml5Mute(): void {\n if (!this._html5GainNode || !this.ctx) return;\n this._html5GainNode.gain.cancelScheduledValues(this.ctx.currentTime);\n this._html5GainNode.gain.setValueAtTime(1, this.ctx.currentTime);\n }\n\n cancelGaplessStart(): void {\n const snap = this._actor.getSnapshot();\n if (snap.context.scheduledStartContextTime === null) return;\n this._actor.send({ type: 'CANCEL_GAPLESS' });\n }\n\n scheduleGaplessStart(when: number): boolean {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return false;\n const state = this._actor.getSnapshot().value;\n if (state !== 'idle' && state !== 'loading') return false;\n this._actor.send({ type: 'SCHEDULE_GAPLESS', when });\n return true;\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTime(): number {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') {\n if (!snap.context.isPlaying) return this.pausedAtTrackTime;\n if (!this.ctx) return 0;\n return Math.max(0, this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * this.queueRef.playbackRate);\n }\n return this.audio.currentTime;\n }\n\n get duration(): number {\n const snap = this._actor.getSnapshot();\n // When playing via HTML5, use audio.duration so it stays consistent\n // with audio.currentTime. audioBuffer.duration can differ (codec padding,\n // container overhead, VBR headers) causing gapless scheduling to start\n // the next track before the HTML5 element actually finishes.\n if (snap.value === 'html5' && !isNaN(this.audio.duration)) {\n return this.audio.duration;\n }\n if (this.audioBuffer) return this.audioBuffer.duration;\n return this.audio.duration;\n }\n\n get isPaused(): boolean {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') return !snap.context.isPlaying;\n return this.audio.paused;\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().context.isPlaying;\n }\n\n get trackUrl(): string {\n return this._resolvedUrl;\n }\n\n get playbackType(): PlaybackType {\n return this._actor.getSnapshot().context.playbackType;\n }\n\n get webAudioLoadingState(): WebAudioLoadingState {\n return this._actor.getSnapshot().context.webAudioLoadingState;\n }\n\n get hasSourceNode(): boolean {\n return this.sourceNode !== null;\n }\n\n get machineState(): string {\n return this._actor.getSnapshot().value as string;\n }\n\n get scheduledStartContextTime(): number | null {\n return this._actor.getSnapshot().context.scheduledStartContextTime;\n }\n\n /**\n * Context-clock time at which the currently playing Web Audio source will\n * reach the end of its buffer, derived from the live playback anchor\n * (_waRefCtxTime/_waRefTrackTime). The anchor is re-established every time\n * a source node starts (scheduled gapless start, resume, seek, crossover),\n * so this stays correct across interruptions — unlike\n * scheduledStartContextTime + duration, which goes stale the moment a\n * pause/resume re-anchors the source at a later context time. For an\n * uninterrupted gapless chain it is exact (the anchor IS the scheduled\n * start), so next-track scheduling stays sample-accurate.\n *\n * Null when no source node is active (idle/html5/loading, or paused).\n */\n get playbackEndContextTime(): number | null {\n const snap = this._actor.getSnapshot();\n if (snap.value !== 'webaudio' || !snap.context.isPlaying) return null;\n if (!this.sourceNode || !this.audioBuffer) return null;\n const rate = this.sourceNode.playbackRate.value || 1;\n const bufferRemaining =\n this.audioBuffer.duration - this._bufferStartPaddingSec - this._waRefTrackTime;\n return this._waRefCtxTime + bufferRemaining / rate;\n }\n\n get isBufferLoaded(): boolean {\n return this.audioBuffer !== null;\n }\n\n toInfo(): TrackInfo {\n return {\n index: this.index,\n currentTime: this.currentTime,\n duration: this.duration,\n isPlaying: this.isPlaying,\n isPaused: this.isPaused,\n volume: this.gainNode?.gain.value ?? this.audio.volume,\n trackUrl: this.trackUrl,\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n metadata: this.metadata,\n playbackRate: this.queueRef.playbackRate,\n machineState: this.machineState,\n };\n }\n\n // --------------------------------------------------------------------------\n // Private: HTML5 helpers\n // --------------------------------------------------------------------------\n\n private _playHtml5(): void {\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n this.audio.playbackRate = this.queueRef.playbackRate;\n const promise = this.audio.play();\n if (promise) {\n promise.catch((err: unknown) => {\n if (err instanceof Error && err.name === 'NotAllowedError') {\n this.queueRef.onPlayBlocked();\n } else if (err instanceof Error && err.name === 'AbortError') {\n // Browser aborted — element will recover on next play()\n } else {\n this.queueRef.onError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n }\n }\n\n private _seekHtml5(): void {\n const clamped = this.pausedAtTrackTime;\n if (!isFinite(clamped)) return;\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) {\n this.audio.currentTime = clamped;\n } else {\n this.audio.addEventListener(\n 'loadedmetadata',\n () => {\n this.audio.currentTime = clamped;\n },\n { once: true }\n );\n this.audio.load();\n }\n }\n\n // --------------------------------------------------------------------------\n // Private: Web Audio helpers\n // --------------------------------------------------------------------------\n\n /**\n * Mid-stream crossover: switch an actively-playing HTML5 track to Web Audio.\n *\n * Why this exists: we cannot reliably predict when an HTML5 <audio> element\n * will fire 'ended' from within the AudioContext clock. Any prediction is\n * at the mercy of the browser's audio pipeline (buffering stalls, codec\n * padding differences, clock drift between audio.currentTime and\n * ctx.currentTime over long sessions). Scheduling the next gapless track\n * against that prediction is how overlap bugs happen.\n *\n * Instead, as soon as the buffer is decoded, we hand playback off to Web\n * Audio while the track is still mid-song. From that point on, the track\n * and all subsequent gapless transitions live on a single clock\n * (AudioContext.currentTime), so scheduling is sample-accurate by\n * construction — no prediction involved.\n *\n * Ordering: pause the HTML5 element FIRST, then start the source node at\n * the captured offset. Pausing first ensures audio.currentTime is frozen\n * before we read it as the Web Audio start offset, so there's no brief\n * double-audio window at the crossover point.\n */\n private _crossoverHtml5ToWebAudio(wasPlaying: boolean): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n\n const htmlTime = this.audio.currentTime;\n const offset =\n isFinite(this.pausedAtTrackTime) && this.pausedAtTrackTime > htmlTime\n ? this.pausedAtTrackTime\n : htmlTime;\n this.pausedAtTrackTime = isFinite(offset) ? offset : 0;\n\n if (!wasPlaying) {\n this.audio.pause();\n this.queueRef.onDebug(\n `crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=false`\n );\n return;\n }\n\n // Start the WebAudio source first to learn the exact context-clock time\n // it will begin producing audio (the \"when\" returned by _startSourceNode).\n // Then align the HTML5 fade-out to that same instant so both crossfade\n // halves overlap at constant-summed gain.\n const when = this._startSourceNode(this.pausedAtTrackTime, CROSSOVER_FADE_SEC);\n\n if (when !== null && this._html5GainNode) {\n this._html5GainNode.gain.cancelScheduledValues(when);\n this._html5GainNode.gain.setValueAtTime(1, when);\n this._html5GainNode.gain.linearRampToValueAtTime(0, when + CROSSOVER_FADE_SEC);\n const ctxRef = this.ctx;\n const html5GainRef = this._html5GainNode;\n const delayMs = (when - this.ctx.currentTime + CROSSOVER_FADE_SEC) * 1000 + 5;\n setTimeout(() => {\n this.audio.pause();\n if (ctxRef && html5GainRef) {\n html5GainRef.gain.cancelScheduledValues(ctxRef.currentTime);\n html5GainRef.gain.setValueAtTime(1, ctxRef.currentTime);\n }\n }, delayMs);\n } else if (!this._html5GainNode) {\n // No MediaElementSource path (CORS blocked or legacy browser).\n // Delay the HTML5 pause until the WebAudio source actually starts\n // at `when` so there's no silence gap during the scheduling lead.\n if (when !== null && this.ctx) {\n const delayMs = (when - this.ctx.currentTime) * 1000;\n setTimeout(() => {\n this.audio.volume = 0;\n this.audio.pause();\n this.audio.volume = 1;\n }, Math.max(0, delayMs));\n } else {\n this.audio.volume = 0;\n this.audio.pause();\n this.audio.volume = 1;\n }\n }\n\n this.queueRef.onDebug(\n `crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=true fade=${CROSSOVER_FADE_SEC}s mediaSource=${!!this._html5GainNode} when=${when?.toFixed(3) ?? 'null'}`\n );\n }\n\n /**\n * Reverted: alignment-based fixes (duration-delta and buffer-silence\n * scanning) reduced the perceived skip on archive.org files but did not\n * eliminate it, suggesting the residual gap isn't a buffer/timeline\n * alignment problem at all. Leaving _bufferStartPaddingSec at 0 (no shift)\n * until we have a confirmed root cause; the field and call sites are kept\n * so we can re-introduce a fix without churning the source-start code.\n */\n private _maybeComputeBufferAlignment(): void {\n if (!this.audioBuffer) return;\n if (this._bufferAlignmentMeasured) return;\n this._bufferStartPaddingSec = 0;\n this._bufferAlignmentMeasured = true;\n this.queueRef.onDebug(\n `_maybeComputeBufferAlignment track=${this.index} bufferDur=${this.audioBuffer.duration.toFixed(3)}s html5Dur=${isNaN(this.audio.duration) ? 'NaN' : this.audio.duration.toFixed(3) + 's'} (alignment shift disabled — see comment)`\n );\n }\n\n private _startSourceNode(offset: number, fadeInSec = 0): number | null {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return null;\n this._maybeComputeBufferAlignment();\n this._stopSourceNode();\n\n const wasSuspended = this.ctx.state === 'suspended';\n if (wasSuspended) {\n this.ctx.resume();\n }\n\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.playbackRate.value = this.queueRef.playbackRate;\n\n // Schedule the source at an explicit future time rather than start(0).\n // start(0) dispatches to the audio render thread asynchronously — the\n // actual start lags the main-thread currentTime read by up to one\n // hardware callback period (5-100 ms depending on device). That δ makes\n // playbackEndContextTime compute an end time δ too early, causing the\n // next gapless track to overlap briefly. Using start(when) with an\n // explicit when that matches _waRefCtxTime eliminates the mismatch.\n const lead = wasSuspended\n ? 0.15\n : Math.max(0.02, 2 * ((this.ctx as unknown as { baseLatency?: number }).baseLatency || 0) + 0.01);\n const when = this.ctx.currentTime + lead;\n\n // Advance the buffer start position by the lead so the source begins\n // from where playback will actually be at `when`, not where it was at\n // the moment we read currentTime. This keeps the currentTime formula\n // (waRefTrackTime + (ctx.currentTime - waRefCtxTime) * rate) equal to\n // `offset` immediately after the call without accumulating drift across\n // pause/resume cycles. During crossover it also aligns the WebAudio\n // source to where the HTML5 element will be at the fade point.\n // The skipped interval (≤20 ms at 1× rate) is below audible threshold.\n const rate = this.queueRef.playbackRate;\n const maxOffset = this.audioBuffer.duration - this._bufferStartPaddingSec;\n const effectiveOffset = Math.min(offset + lead * rate, maxOffset);\n\n if (fadeInSec > 0) {\n const fadeNode = this.ctx.createGain();\n fadeNode.gain.setValueAtTime(0, when);\n fadeNode.gain.linearRampToValueAtTime(1, when + fadeInSec);\n this.sourceNode.connect(fadeNode);\n fadeNode.connect(this.gainNode);\n } else {\n this.sourceNode.connect(this.gainNode);\n }\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this._waRefCtxTime = when;\n this._waRefTrackTime = effectiveOffset;\n this.sourceNode.start(when, effectiveOffset + this._bufferStartPaddingSec);\n\n // Hard ceiling: stop the source at its computed end time so any\n // platform-level late start (render thread couldn't honor `when`)\n // produces an inaudible truncation instead of overlap with the next track.\n const bufRemaining = this.audioBuffer.duration - this._bufferStartPaddingSec - effectiveOffset;\n if (bufRemaining > 0) {\n this.sourceNode.stop(when + bufRemaining / rate);\n }\n return when;\n }\n\n private _stopSourceNode(): void {\n if (!this.sourceNode) return;\n this.sourceNode.onended = null;\n try {\n this.sourceNode.stop();\n } catch {\n /* already stopped */\n }\n try {\n this.sourceNode.disconnect();\n } catch {\n /* already disconnected */\n }\n this.sourceNode = null;\n }\n\n private _seekWebAudio(): void {\n const snap = this._actor.getSnapshot();\n const wasPlaying = snap.context.isPlaying;\n const clamped = this.pausedAtTrackTime;\n this._stopSourceNode();\n if (wasPlaying) {\n this._startSourceNode(clamped);\n }\n }\n\n private _handleWebAudioEnded = (): void => {\n this.queueRef.onDebug(\n `_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n if (!this.sourceNode) return;\n this._actor.send({ type: 'WEBAUDIO_ENDED' });\n };\n\n // --------------------------------------------------------------------------\n // Private: progress loop (requestAnimationFrame)\n // --------------------------------------------------------------------------\n\n startProgressLoop(): void {\n if (this.rafId !== null) return;\n const loop = () => {\n if (this.isPaused || !this.isPlaying) {\n this.rafId = null;\n return;\n }\n this.queueRef.onProgress(this.toInfo());\n\n const remaining = this.duration - this.currentTime;\n const snap = this._actor.getSnapshot();\n if (\n !snap.context.notifiedLookahead &&\n !isNaN(remaining) &&\n remaining <= GAPLESS_SCHEDULE_LOOKAHEAD\n ) {\n this._actor.send({ type: 'LOOKAHEAD_REACHED' });\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }\n\n const preloadThreshold = isNaN(this.duration) ? PRELOAD_DELAY : Math.min(this.duration * 0.2, PRELOAD_DELAY);\n if (!this._notifiedPreloadThreshold && this.currentTime >= preloadThreshold) {\n this._notifiedPreloadThreshold = true;\n queueMicrotask(() => this.queueRef.onPreloadReady(this));\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n }\n\n private _stopProgressLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n}\n","// ---------------------------------------------------------------------------\n// TrackMachine — per-track audio state (xstate v5)\n//\n// States:\n// idle Initial state. Audio nodes not yet initialised.\n// html5 HTML5 Audio is playing. Web Audio fetch+decode is in progress.\n// loading Track is preloaded (not yet playing). Decode in progress.\n// webaudio AudioBufferSourceNode is the active output.\n//\n// Design invariant — \"Web Audio always wins as soon as the buffer is ready\":\n// When a track's buffer finishes decoding (BUFFER_READY) while the track is\n// playing via HTML5, we hand playback off to Web Audio mid-stream. The\n// HTML5 element is paused, and an AudioBufferSourceNode is started at the\n// exact offset the HTML5 element was at. From that instant on, the track\n// (and every gapless transition that follows it) lives on one clock\n// (AudioContext.currentTime).\n//\n// This is fundamental to gapless correctness: we cannot predict when an\n// HTML5 element will fire 'ended' from the AudioContext clock — the two\n// run on independent clocks with independent jitter (buffering stalls,\n// codec padding differences, long-session drift). Any gapless scheduling\n// that crosses those clocks is a prediction, and predictions are how\n// overlap bugs happen. By moving the current track to Web Audio as soon\n// as we can, all future gapless transitions are WebAudio→WebAudio and\n// sample-accurate by construction.\n//\n// DEACTIVATE transitions from every playing state land in idle, so a\n// deactivated track with a loaded buffer is always idle+LOADED — ready\n// for Web Audio on re-play.\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, spawnChild } from 'xstate';\nimport { fetchDecodeMachine } from './fetchDecode.machine';\nimport type { WebAudioLoadingState, PlaybackType } from '../types';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface TrackContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n playbackType: PlaybackType;\n webAudioLoadingState: WebAudioLoadingState;\n isPlaying: boolean;\n scheduledStartContextTime: number | null;\n notifiedLookahead: boolean;\n fetchStarted: boolean;\n /** True when PLAY was received in webAudioOnly mode before buffer is ready. */\n pendingPlay: boolean;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type TrackEvent =\n | { type: 'ACTIVATE' }\n | { type: 'DEACTIVATE' }\n | { type: 'PRELOAD' }\n | { type: 'BUFFER_LOADING' }\n | { type: 'PLAY' }\n | { type: 'PLAY_WEBAUDIO' }\n | { type: 'PAUSE' }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'BUFFER_READY' }\n | { type: 'BUFFER_ERROR' }\n | { type: 'HTML5_ENDED' }\n | { type: 'WEBAUDIO_ENDED' }\n | { type: 'URL_RESOLVED'; url: string }\n | { type: 'START_FETCH' }\n | { type: 'SCHEDULE_GAPLESS'; when: number }\n | { type: 'CANCEL_GAPLESS' }\n | { type: 'LOOKAHEAD_REACHED' };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createTrackMachine(initialContext: TrackContext) {\n return setup({\n types: {\n context: {} as TrackContext,\n events: {} as TrackEvent,\n },\n actors: {\n fetchDecode: fetchDecodeMachine,\n },\n guards: {\n canPlayWebAudio: () => false,\n isWebAudioOnly: () => false,\n canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && !context.fetchStarted,\n },\n actions: {\n playHtml5: () => {},\n startSourceNode: () => {},\n crossoverHtml5ToWebAudio: () => {},\n notifyBufferReady: () => {},\n startScheduledSourceNode: () => {},\n startProgressLoop: () => {},\n pauseHtml5: () => {},\n freezePausedTime: () => {},\n stopSourceNode: () => {},\n stopProgressLoop: () => {},\n reportProgress: () => {},\n seekHtml5: () => {},\n seekWebAudio: () => {},\n resetHtml5Element: () => {},\n resetTiming: () => {},\n notifyTrackEnded: () => {},\n triggerFetchForPendingPlay: () => {},\n setPendingPlay: assign({ pendingPlay: () => true }),\n clearPendingPlay: assign({ pendingPlay: () => false }),\n setIsPlaying: assign({ isPlaying: () => true }),\n clearIsPlaying: assign({ isPlaying: () => false }),\n setLoadingState: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),\n setLoadedState: assign({ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState }),\n setErrorState: assign({ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState }),\n clearScheduleAndLookahead: assign({ scheduledStartContextTime: () => null, notifiedLookahead: () => false }),\n setPlayingWebAudio: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n setScheduledGapless: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n scheduledStartContextTime: ({ event }) =>\n (event as { type: 'SCHEDULE_GAPLESS'; when: number }).when,\n }),\n clearPlayingAndSchedule: assign({\n isPlaying: () => false,\n scheduledStartContextTime: () => null,\n notifiedLookahead: () => false,\n }),\n setNotifiedLookahead: assign({ notifiedLookahead: () => true }),\n setResolvedUrl: assign({\n resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,\n }),\n clearScheduledStart: assign({ scheduledStartContextTime: () => null }),\n setPlayingWebAudioType: assign({\n isPlaying: () => true,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n setPlaybackTypeWebAudio: assign({\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n clearNotifiedLookahead: assign({\n notifiedLookahead: () => false,\n }),\n },\n }).createMachine({\n id: 'track',\n initial: 'idle',\n context: initialContext,\n\n on: {\n START_FETCH: {\n guard: 'canStartFetch',\n actions: [\n assign({\n webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState,\n fetchStarted: () => true,\n }),\n spawnChild('fetchDecode', {\n id: 'fetchDecode',\n input: ({ context }) => ({\n trackUrl: context.trackUrl,\n resolvedUrl: context.resolvedUrl,\n skipHEAD: context.skipHEAD,\n }),\n }),\n ],\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle: constructed but not started\n // -----------------------------------------------------------------\n idle: {\n on: {\n HTML5_ENDED: {\n actions: ['notifyTrackEnded'],\n },\n DEACTIVATE: {\n actions: [\n 'pauseHtml5',\n 'resetHtml5Element',\n 'resetTiming',\n 'stopProgressLoop',\n 'clearScheduleAndLookahead',\n 'clearPendingPlay',\n ],\n },\n ACTIVATE: {\n actions: [\n 'resetTiming',\n 'resetHtml5Element',\n 'clearScheduleAndLookahead',\n 'clearPendingPlay',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n guard: 'isWebAudioOnly',\n actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],\n },\n {\n target: 'html5',\n // triggerFetchForPendingPlay also kicks off fetch+decode for the\n // CURRENT track, not just the next one. That way BUFFER_READY\n // fires while we're in html5, and crossoverHtml5ToWebAudio can\n // hand the active track over to Web Audio mid-stream. The\n // canStartFetch guard inside START_FETCH makes this a no-op if\n // a fetch is already in flight (e.g. PLAY from loading state).\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop', 'triggerFetchForPendingPlay'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n PRELOAD: { target: 'loading' },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: [\n {\n guard: ({ context }: { context: TrackContext }) => context.pendingPlay,\n target: 'webaudio',\n actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop', 'notifyBufferReady'],\n },\n {\n actions: ['setLoadedState', 'notifyBufferReady'],\n },\n ],\n BUFFER_ERROR: {\n actions: ['setErrorState', 'clearPendingPlay'],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // html5: HTML5 Audio is playing; WebAudio decode may be in progress\n // -----------------------------------------------------------------\n html5: {\n on: {\n PAUSE: {\n actions: ['clearIsPlaying', 'pauseHtml5', 'stopProgressLoop', 'reportProgress'],\n },\n PLAY: {\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: 'setPlayingWebAudio',\n },\n // Mid-stream crossover to Web Audio. Running `crossoverHtml5ToWebAudio`\n // synchronously captures audio.currentTime, pauses the HTML5 element,\n // and starts a Web Audio source node at that exact offset. After the\n // transition completes we're in webaudio state with isPlaying preserved\n // (true if the track was playing, false if paused), so a subsequent\n // PLAY in webaudio state will resume from pausedAtTrackTime.\n //\n // clearNotifiedLookahead resets the gapless lookahead flag so the\n // webaudio progress loop can re-trigger scheduling with the accurate\n // shared-clock end time, replacing any stale HTML5-clock prediction.\n BUFFER_READY: {\n target: 'webaudio',\n actions: [\n 'setLoadedState',\n 'crossoverHtml5ToWebAudio',\n 'setPlaybackTypeWebAudio',\n 'clearNotifiedLookahead',\n 'notifyBufferReady',\n ],\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n SEEK: {\n actions: [\n 'seekHtml5',\n 'reportProgress',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n HTML5_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'pauseHtml5',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'pauseHtml5', 'resetHtml5Element', 'resetTiming', 'stopProgressLoop'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // loading: preloading in background (not the active track yet)\n // -----------------------------------------------------------------\n loading: {\n on: {\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: [\n {\n guard: ({ context }: { context: TrackContext }) => context.pendingPlay,\n target: 'webaudio',\n actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop', 'notifyBufferReady'],\n },\n {\n target: 'idle',\n actions: ['setLoadedState', 'notifyBufferReady'],\n },\n ],\n BUFFER_ERROR: {\n target: 'idle',\n actions: ['setErrorState', 'clearPendingPlay'],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n guard: 'isWebAudioOnly',\n actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],\n },\n {\n target: 'html5',\n // triggerFetchForPendingPlay also kicks off fetch+decode for the\n // CURRENT track, not just the next one. That way BUFFER_READY\n // fires while we're in html5, and crossoverHtml5ToWebAudio can\n // hand the active track over to Web Audio mid-stream. The\n // canStartFetch guard inside START_FETCH makes this a no-op if\n // a fetch is already in flight (e.g. PLAY from loading state).\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop', 'triggerFetchForPendingPlay'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'clearPendingPlay',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'clearPendingPlay', 'resetTiming'],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // webaudio: AudioBufferSourceNode is driving output\n // -----------------------------------------------------------------\n webaudio: {\n on: {\n // clearScheduledStart: once the source node is stopped, the\n // scheduled start time no longer describes anything real — resume\n // re-anchors the source at a later context time, so any end-time\n // math based on scheduledStartContextTime + duration would land\n // mid-track (scheduling the next track to start while this one is\n // still audible).\n PAUSE: {\n actions: [\n 'clearIsPlaying',\n 'freezePausedTime',\n 'clearScheduledStart',\n 'stopSourceNode',\n 'stopProgressLoop',\n 'reportProgress',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n actions: ['setIsPlaying', 'startSourceNode', 'startProgressLoop'],\n },\n {\n actions: 'setIsPlaying',\n },\n ],\n PLAY_WEBAUDIO: {\n actions: 'setPlayingWebAudioType',\n },\n SEEK: {\n actions: [\n 'clearScheduledStart',\n 'seekWebAudio',\n 'reportProgress',\n ],\n },\n SET_VOLUME: {},\n CANCEL_GAPLESS: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'stopProgressLoop',\n 'resetTiming',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n WEBAUDIO_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n // Bug #3 fix: DEACTIVATE from webaudio → idle (was staying in webaudio)\n DEACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'resetTiming',\n 'resetHtml5Element',\n 'stopProgressLoop',\n ],\n },\n },\n },\n },\n });\n}\n\nexport type TrackMachine = ReturnType<typeof createTrackMachine>;\n","// ---------------------------------------------------------------------------\n// FetchDecodeMachine — child actor for the fetch + decode pipeline\n//\n// States: resolvingUrl → fetching → decoding → done | error\n//\n// Spawned by TrackMachine via START_FETCH. Sends BUFFER_READY, BUFFER_ERROR,\n// and URL_RESOLVED back to the parent — the same events TrackMachine already\n// handles. Promise implementations are no-op defaults, provided by Track.ts\n// via .provide().\n//\n// xstate v5 automatically passes an AbortSignal to fromPromise actors, so\n// when the parent stops (destroy), in-flight fetches are aborted for free.\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, sendParent, fromPromise } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface FetchDecodeContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n}\n\n// ---- Machine ---------------------------------------------------------------\n\nexport const fetchDecodeMachine = setup({\n types: {\n context: {} as FetchDecodeContext,\n input: {} as FetchDecodeContext,\n },\n actors: {\n resolveUrl: fromPromise<string | null, { trackUrl: string }>(async () => null),\n fetchAudio: fromPromise<void, { resolvedUrl: string }>(async () => {}),\n decodeAudio: fromPromise<void, void>(async () => {}),\n },\n guards: {\n shouldSkipHEAD: ({ context }) => context.skipHEAD,\n },\n}).createMachine({\n id: 'fetchDecode',\n initial: 'resolvingUrl',\n context: {\n trackUrl: '',\n resolvedUrl: '',\n skipHEAD: false,\n },\n entry: assign(({ event }) => {\n const { input } = event as { type: string; input: FetchDecodeContext };\n return {\n trackUrl: input.trackUrl,\n resolvedUrl: input.resolvedUrl,\n skipHEAD: input.skipHEAD,\n };\n }),\n\n states: {\n // -----------------------------------------------------------------\n // resolvingUrl: HEAD request to resolve redirects\n // -----------------------------------------------------------------\n resolvingUrl: {\n always: {\n guard: 'shouldSkipHEAD',\n target: 'fetching',\n },\n invoke: {\n id: 'resolveUrl',\n src: 'resolveUrl',\n input: ({ context }) => ({ trackUrl: context.trackUrl }),\n onDone: {\n target: 'fetching',\n actions: [\n assign({\n resolvedUrl: ({ event, context }) => event.output ?? context.resolvedUrl,\n skipHEAD: () => true,\n }),\n sendParent(({ event, context }) => {\n const url = event.output;\n return { type: 'URL_RESOLVED' as const, url: url ?? context.resolvedUrl };\n }),\n ],\n },\n onError: {\n // HEAD failed — non-fatal, fall back to original URL\n target: 'fetching',\n actions: assign({ skipHEAD: () => true }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // fetching: GET the audio data\n // -----------------------------------------------------------------\n fetching: {\n invoke: {\n id: 'fetchAudio',\n src: 'fetchAudio',\n input: ({ context }) => ({ resolvedUrl: context.resolvedUrl }),\n onDone: 'decoding',\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // decoding: decodeAudioData on the ArrayBuffer\n // -----------------------------------------------------------------\n decoding: {\n invoke: {\n id: 'decodeAudio',\n src: 'decodeAudio',\n input: () => {},\n onDone: {\n target: 'done',\n actions: sendParent({ type: 'BUFFER_READY' }),\n },\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // Terminal states\n // -----------------------------------------------------------------\n done: { type: 'final' },\n error: { type: 'final' },\n },\n});\n\nexport type FetchDecodeMachine = typeof fetchDecodeMachine;\n","/** Returns a throttled version of `fn` that fires at most once per `ms`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(\n fn: T,\n ms: number,\n): (...args: Parameters<T>) => void {\n let last = 0;\n return (...args: Parameters<T>) => {\n const now = performance.now();\n if (now - last < ms) return;\n last = now;\n fn(...args);\n };\n}\n"],"mappings":"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,oBACA,mBACA,qBACA,yBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,SAAU,CACR,QAAS,CACP,oBACA,mBACA,oBACA,yBACA,sBACA,0BACA,6BACA,cACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,QAAS,CACP,oBACA,mBACA,iBACA,yBACA,yBACA,sBACA,6BACA,cACF,CACF,EACA,CACE,QAAS,CACP,oBACA,mBACA,iBACA,yBACA,cACF,CACF,CACF,EACA,KAAM,CACJ,QAAS,CAAC,cAAe,4BAA4B,CACvD,EACA,YAAa,CACX,CACE,MAAO,eACP,OAAQ,UACR,QAAS,CACP,uBACA,oBACA,wBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,CACE,OAAQ,QACR,QAAS,CAAC,uBAAwB,aAAa,CACjD,CACF,EACA,aAAc,CACZ,CAME,MAAO,CAAC,CAAE,QAAAC,EAAS,MAAAC,CAAM,IAAMA,EAAM,QAAUD,EAAQ,kBACvD,QAAS,CAAC,6BAA8B,cAAc,CACxD,EACA,CACE,QAAS,CAAC,kBAAmB,cAAc,CAC7C,CACF,CACF,CACF,EAKA,OAAQ,CACN,GAAI,CACF,aAAc,CACZ,CACE,MAAO,cACP,OAAQ,OACR,QAAS,CAAC,mBAAoB,qBAAqB,CACrD,EACA,CACE,MAAO,oBACP,QAAS,CAAC,mBAAoB,sBAAuB,cAAc,CACrE,EACA,CACE,QAAS,CAAC,sBAAuB,cAAc,CACjD,CACF,EACA,KAAM,CACJ,OAAQ,UACR,QAAS,CAAC,cAAe,6BAA8B,eAAgB,iBAAiB,CAC1F,EACA,OAAQ,CACN,OAAQ,UACR,QAAS,CAAC,cAAe,6BAA8B,eAAgB,iBAAiB,CAC1F,EACA,KAAM,CACJ,QAAS,CACP,oBACA,mBACA,qBACA,yBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,SAAU,CACR,QAAS,CACP,oBACA,mBACA,oBACA,yBACA,sBACA,0BACA,6BACA,cACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,UACR,QAAS,CACP,oBACA,mBACA,iBACA,yBACA,yBACA,sBA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","names":["createActor","isBrowser","_audioContext","createAudioContext","Ctor","getAudioContext","resumeAudioContext","ctx","hasMediaSession","setupMediaSession","handlers","mediaSession","details","updateMediaSessionMetadata","metadata","updateMediaSessionPlaybackState","isPlaying","updateMediaSessionPositionState","duration","position","playbackRate","setup","assign","createQueueMachine","initialContext","context","event","e","newCount","next","createActor","fromPromise","setup","assign","spawnChild","setup","assign","sendParent","fromPromise","fetchDecodeMachine","context","event","input","createTrackMachine","initialContext","setup","fetchDecodeMachine","context","assign","event","spawnChild","GAPLESS_SCHEDULE_LOOKAHEAD","PRELOAD_DELAY","CROSSOVER_FADE_SEC","Track","context","getAudioContext","opts","_a","_b","code","msg","initialContext","machine","createTrackMachine","fetchDecodeMachine","fromPromise","signal","res","input","resolvedUrl","buf","resumeAudioContext","when","bufRemaining","schedRate","t","createActor","time","clamped","v","vol","rate","oldRate","remaining","secondsFromEnd","dur","state","snap","bufferRemaining","promise","err","wasPlaying","htmlTime","offset","ctxRef","html5GainRef","delayMs","fadeInSec","wasSuspended","lead","maxOffset","effectiveOffset","fadeNode","loop","preloadThreshold","throttle","fn","ms","last","args","now","MAX_SCHEDULE_LOOKAHEAD","_silentWavUrl","getSilentWavUrl","sampleRate","numSamples","fileSize","buffer","view","writeStr","off","s","i","Queue","throttle","duration","currentTime","playbackRate","updateMediaSessionPositionState","options","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","onError","onPlayBlocked","onDebug","playbackMethod","trackMetadata","initialVolume","preloadNumTracks","initialPlaybackRate","url","Track","machine","createQueueMachine","context","_a","track","cur","updateMediaSessionMetadata","event","e","createActor","snapshot","playing","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","startTime","time","volume","clamped","rate","snap","metadata","resumeAudioContext","value","newSnap","info","error","msg","fromIndex","curBelowThreshold","threshold","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","timeUntilEnd","anchoredEnd","remaining"]}
|