gapless 4.2.0 → 4.2.1
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/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Track.ts +11 -0
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createActor as j}from"xstate";var q=typeof window<"u",p;function V(){if(!q)return null;let o=window.AudioContext??window.webkitAudioContext;return o?new o:null}function k(){return p===void 0?null:p}function y(){p===void 0&&(p=V());let o=p;return o&&o.state==="suspended"?o.resume():Promise.resolve()}var g=typeof navigator<"u"&&"mediaSession"in navigator;function P(o){if(!g)return;let{mediaSession:e}=navigator;e.setActionHandler("play",o.onPlay),e.setActionHandler("pause",o.onPause),e.setActionHandler("nexttrack",o.onNext),e.setActionHandler("previoustrack",o.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&o.onSeek(t.seekTime)})}function b(o){if(g){if(!o){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:o.title??"",artist:o.artist??"",album:o.album??"",artwork:o.artwork??[]})}}function N(o){g&&(navigator.mediaSession.playbackState=o?"playing":"paused")}function R(o,e,t=1){if(g)try{navigator.mediaSession.setPositionState({duration:o,position:e,playbackRate:t})}catch{}}import{setup as K,assign as h}from"xstate";function C(o){return K({types:{context:{},events:{}},guards:{hasNextTrack:({context:e})=>e.currentTrackIndex+1<e.trackCount,playImmediately:({event:e})=>!!e.playImmediately,willBeEmpty:({context:e})=>e.trackCount<=1,isRemovingCurrent:({context:e,event:t})=>t.index===e.currentTrackIndex},actions:{incrementTrackCount:h({trackCount:({context:e})=>e.trackCount+1}),decrementTrackCount:h({trackCount:({context:e})=>Math.max(0,e.trackCount-1),currentTrackIndex:({context:e,event:t})=>{let r=t,a=Math.max(0,e.trackCount-1);return r.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):a>0?Math.min(e.currentTrackIndex,a-1):0}}),gotoTrackIndex:h({currentTrackIndex:({event:e})=>e.index}),advanceToNextTrack:h({currentTrackIndex:({context:e})=>{let t=e.currentTrackIndex+1;return t<e.trackCount?t:e.currentTrackIndex}}),goToPreviousTrack:h({currentTrackIndex:({context:e})=>Math.max(0,e.currentTrackIndex-1)}),advanceOnTrackEnd:h({currentTrackIndex:({context:e})=>Math.min(e.currentTrackIndex+1,e.trackCount-1)}),resetToFirstTrack:h({currentTrackIndex:()=>0}),deactivateCurrent:()=>{},deactivateEndedTrack:()=>{},activateAndPlayCurrent:()=>{},playOrContinueGapless:()=>{},cancelAllGapless:()=>{},notifyStartNewTrack:()=>{},notifyPlayNextTrack:()=>{},notifyPlayPreviousTrack:()=>{},notifyEnded:()=>{},updateMediaSessionMetadata:()=>{},preloadAhead:()=>{},playCurrent:()=>{},pauseCurrent:()=>{},seekCurrent:()=>{},seekCurrentToZero:()=>{},scheduleGapless:()=>{},cancelScheduledGapless:()=>{},cancelAndRescheduleGapless:()=>{}}}).createMachine({id:"queue",initial:"idle",context:o,on:{ADD_TRACK:{actions:"incrementTrackCount"},REMOVE_TRACK:{actions:"decrementTrackCount"}},states:{idle:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},playing:{on:{REMOVE_TRACK:[{guard:"willBeEmpty",target:"idle",actions:["cancelAllGapless","decrementTrackCount"]},{guard:"isRemovingCurrent",actions:["cancelAllGapless","decrementTrackCount","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["decrementTrackCount","cancelAndRescheduleGapless","preloadAhead"]}],PAUSE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},TOGGLE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent","cancelAndRescheduleGapless"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"playing",actions:["deactivateEndedTrack","advanceOnTrackEnd","playOrContinueGapless","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:[{guard:({context:e,event:t})=>t.index===e.currentTrackIndex,actions:["cancelAndRescheduleGapless","preloadAhead"]},{actions:["scheduleGapless","preloadAhead"]}]}},paused:{on:{REMOVE_TRACK:[{guard:"willBeEmpty",target:"idle",actions:["cancelAllGapless","decrementTrackCount"]},{guard:"isRemovingCurrent",actions:["cancelAllGapless","decrementTrackCount","preloadAhead"]},{actions:["decrementTrackCount","preloadAhead"]}],PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},TOGGLE:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"paused",actions:["deactivateEndedTrack","advanceOnTrackEnd","notifyStartNewTrack","updateMediaSessionMetadata"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},ended:{on:{ADD_TRACK:{target:"paused",actions:"incrementTrackCount"},PLAY:{target:"playing",actions:["resetToFirstTrack","playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}]}}}})}import{createActor as X,fromPromise as x}from"xstate";import{setup as Q,assign as c,spawnChild as Z}from"xstate";import{setup as Y,assign as E,sendParent as T,fromPromise as S}from"xstate";var m=Y({types:{context:{},input:{}},actors:{resolveUrl:S(async()=>null),fetchAudio:S(async()=>{}),decodeAudio:S(async()=>{})},guards:{shouldSkipHEAD:({context:o})=>o.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:{trackUrl:"",resolvedUrl:"",skipHEAD:!1},entry:E(({event:o})=>{let{input:e}=o;return{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:o})=>({trackUrl:o.trackUrl}),onDone:{target:"fetching",actions:[E({resolvedUrl:({event:o,context:e})=>o.output??e.resolvedUrl,skipHEAD:()=>!0}),T(({event:o,context:e})=>({type:"URL_RESOLVED",url:o.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:E({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:o})=>({resolvedUrl:o.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:T({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:T({type:"BUFFER_READY"})},onError:{target:"error",actions:T({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function D(o){return Q({types:{context:{},events:{}},actors:{fetchDecode:m},guards:{canPlayWebAudio:()=>!1,isWebAudioOnly:()=>!1,canStartFetch:({context:e})=>e.webAudioLoadingState==="NONE"&&!e.fetchStarted},actions:{playHtml5:()=>{},startSourceNode:()=>{},crossoverHtml5ToWebAudio:()=>{},notifyBufferReady:()=>{},startScheduledSourceNode:()=>{},startProgressLoop:()=>{},pauseHtml5:()=>{},freezePausedTime:()=>{},stopSourceNode:()=>{},stopProgressLoop:()=>{},reportProgress:()=>{},seekHtml5:()=>{},seekWebAudio:()=>{},resetHtml5Element:()=>{},resetTiming:()=>{},notifyTrackEnded:()=>{},triggerFetchForPendingPlay:()=>{},setPendingPlay:c({pendingPlay:()=>!0}),clearPendingPlay:c({pendingPlay:()=>!1}),setIsPlaying:c({isPlaying:()=>!0}),clearIsPlaying:c({isPlaying:()=>!1}),setLoadingState:c({webAudioLoadingState:()=>"LOADING"}),setLoadedState:c({webAudioLoadingState:()=>"LOADED"}),setErrorState:c({webAudioLoadingState:()=>"ERROR"}),clearScheduleAndLookahead:c({scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setPlayingWebAudio:c({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO"}),setScheduledGapless:c({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO",scheduledStartContextTime:({event:e})=>e.when}),clearPlayingAndSchedule:c({isPlaying:()=>!1,scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setNotifiedLookahead:c({notifiedLookahead:()=>!0}),setResolvedUrl:c({resolvedUrl:({event:e})=>e.url}),clearScheduledStart:c({scheduledStartContextTime:()=>null}),setPlayingWebAudioType:c({isPlaying:()=>!0,playbackType:()=>"WEBAUDIO"}),setPlaybackTypeWebAudio:c({playbackType:()=>"WEBAUDIO"}),clearNotifiedLookahead:c({notifiedLookahead:()=>!1})}}).createMachine({id:"track",initial:"idle",context:o,on:{START_FETCH:{guard:"canStartFetch",actions:[c({webAudioLoadingState:()=>"LOADING",fetchStarted:()=>!0}),Z("fetchDecode",{id:"fetchDecode",input:({context:e})=>({trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD})})]}},states:{idle:{on:{HTML5_ENDED:{actions:["notifyTrackEnded"]},DEACTIVATE:{actions:["pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop","clearScheduleAndLookahead"]},ACTIVATE:{actions:["resetTiming","resetHtml5Element","clearScheduleAndLookahead"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{guard:"isWebAudioOnly",actions:["setPendingPlay","triggerFetchForPendingPlay"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop","triggerFetchForPendingPlay"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},PRELOAD:{target:"loading"},BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:[{guard:({context:e})=>e.pendingPlay,target:"webaudio",actions:["clearPendingPlay","setPlayingWebAudio","startSourceNode","startProgressLoop","notifyBufferReady"]},{actions:["setLoadedState","notifyBufferReady"]}],BUFFER_ERROR:{actions:["setErrorState","clearPendingPlay"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},html5:{on:{PAUSE:{actions:["clearIsPlaying","pauseHtml5","stopProgressLoop","reportProgress"]},PLAY:{actions:["setIsPlaying","playHtml5","startProgressLoop"]},BUFFER_LOADING:{actions:"setLoadingState"},PLAY_WEBAUDIO:{target:"webaudio",actions:"setPlayingWebAudio"},BUFFER_READY:{target:"webaudio",actions:["setLoadedState","crossoverHtml5ToWebAudio","setPlaybackTypeWebAudio","clearNotifiedLookahead","notifyBufferReady"]},BUFFER_ERROR:{actions:"setErrorState"},SEEK:{actions:["seekHtml5","reportProgress"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},HTML5_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","pauseHtml5","stopProgressLoop","resetTiming","resetHtml5Element"]},URL_RESOLVED:{actions:"setResolvedUrl"},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop"]}}},loading:{on:{BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:[{guard:({context:e})=>e.pendingPlay,target:"webaudio",actions:["clearPendingPlay","setPlayingWebAudio","startSourceNode","startProgressLoop","notifyBufferReady"]},{target:"idle",actions:["setLoadedState","notifyBufferReady"]}],BUFFER_ERROR:{target:"idle",actions:["setErrorState","clearPendingPlay"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{guard:"isWebAudioOnly",actions:["setPendingPlay","triggerFetchForPendingPlay"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop","triggerFetchForPendingPlay"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","resetTiming"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},webaudio:{on:{PAUSE:{actions:["clearIsPlaying","freezePausedTime","stopSourceNode","stopProgressLoop","reportProgress"]},PLAY:[{guard:"canPlayWebAudio",actions:["setIsPlaying","startSourceNode","startProgressLoop"]},{actions:"setIsPlaying"}],PLAY_WEBAUDIO:{actions:"setPlayingWebAudioType"},SEEK:{actions:["clearScheduledStart","seekWebAudio","reportProgress"]},SET_VOLUME:{},CANCEL_GAPLESS:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","stopProgressLoop","resetTiming"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},WEBAUDIO_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","stopProgressLoop","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","resetTiming","resetHtml5Element","stopProgressLoop"]}}}}})}var z=5,L=15,A=.03,f=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_playbackMethod;get ctx(){if(this._playbackMethod==="HTML5_ONLY")return null;let e=k();if(e&&!this.gainNode){this.gainNode=e.createGain(),this.gainNode.gain.value=this.audio.volume,this.gainNode.connect(e.destination);try{this._mediaElementSource=e.createMediaElementSource(this.audio),this._html5GainNode=e.createGain(),this._html5GainNode.gain.value=1,this._mediaElementSource.connect(this._html5GainNode),this._html5GainNode.connect(this.gainNode),this.audio.volume=1}catch{this._mediaElementSource=null,this._html5GainNode=null}}return e}gainNode=null;_mediaElementSource=null;_html5GainNode=null;sourceNode=null;audioBuffer=null;_bufferStartPaddingSec=0;_bufferAlignmentMeasured=!1;_waRefCtxTime=0;_waRefTrackTime=0;pausedAtTrackTime=0;_actor;queueRef;rafId=null;_notifiedPreloadThreshold=!1;constructor(e){this.index=e.index,this._trackUrl=e.trackUrl,this._resolvedUrl=e.trackUrl,this.skipHEAD=e.skipHEAD??!1,this.metadata=e.metadata??{},this.queueRef=e.queue,this.audio=new Audio,this.audio.preload="none",this.audio.src=this._trackUrl,this.audio.volume=e.queue.volume,this.audio.controls=!1,this.audio.onerror=()=>{var d,u;let a=(d=this.audio.error)==null?void 0:d.code;if(a===1)return;let s=((u=this.audio.error)==null?void 0:u.message)??"unknown";this.queueRef.onError(new Error(`HTML5 audio error on track ${this.index} (code ${a}): ${s}`))},this.audio.onended=()=>{this.queueRef.onDebug(`audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`),this._actor.send({type:"HTML5_ENDED"})},this._playbackMethod=e.queue.playbackMethod;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchStarted:!1,pendingPlay:!1},r=D(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode),isWebAudioOnly:()=>this._playbackMethod==="WEBAUDIO_ONLY"},actors:{fetchDecode:m.provide({actors:{resolveUrl:x(async({signal:a})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:a});return s.redirected&&s.url?(this._resolvedUrl=s.url,s.url):null}),fetchAudio:x(async({input:a,signal:s})=>{let{resolvedUrl:d}=a,u=await fetch(d,{signal:s});if(!u.ok)throw new Error(`HTTP ${u.status} for ${d}`);this._pendingArrayBuffer=await u.arrayBuffer()}),decodeAudio:x(async()=>{let a=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!a||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(a),this._maybeComputeBufferAlignment()})}})},actions:{triggerFetchForPendingPlay:()=>{this.preload(),y()},playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},crossoverHtml5ToWebAudio:({context:a})=>{this._crossoverHtml5ToWebAudio(a.isPlaying)},startScheduledSourceNode:({context:a})=>{let s=a.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._maybeComputeBufferAlignment(),this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.playbackRate.value=this.queueRef.playbackRate,this.sourceNode.connect(this.gainNode),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,this._bufferStartPaddingSec),this._waRefCtxTime=s,this._waRefTrackTime=0,this.queueRef.onDebug(`startScheduledSourceNode track=${this.index} when=${s.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(s-this.ctx.currentTime).toFixed(3)}s padding=${this._bufferStartPaddingSec.toFixed(3)}s`))},startProgressLoop:()=>this.startProgressLoop(),pauseHtml5:()=>this.audio.pause(),freezePausedTime:()=>{let a=this.currentTime;this.pausedAtTrackTime=isFinite(a)?a:0},stopSourceNode:()=>this._stopSourceNode(),stopProgressLoop:()=>this._stopProgressLoop(),reportProgress:()=>{queueMicrotask(()=>this.queueRef.onProgress(this.toInfo()))},seekHtml5:()=>this._seekHtml5(),seekWebAudio:()=>this._seekWebAudio(),resetHtml5Element:()=>{this.audio.currentTime=0},resetTiming:()=>{this._waRefCtxTime=0,this._waRefTrackTime=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))},notifyBufferReady:()=>{queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))}}});this._actor=X(r),this._actor.start()}play(){this.queueRef.onDebug(`Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`),this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}seek(e){if(!isFinite(e))return;let t=Math.max(0,isNaN(this.duration)?e:Math.min(e,this.duration));this.pausedAtTrackTime=t,this._actor.send({type:"SEEK",time:t})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._mediaElementSource?this.audio.volume=1:this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t),this._actor.send({type:"SET_VOLUME",volume:t})}setPlaybackRate(e){if(this.ctx&&this.sourceNode&&this._actor.getSnapshot().context.isPlaying){let t=this.sourceNode.playbackRate.value;this._waRefTrackTime=this._waRefTrackTime+(this.ctx.currentTime-this._waRefCtxTime)*t,this._waRefCtxTime=this.ctx.currentTime}this.audio.playbackRate=e,this.sourceNode&&(this.sourceNode.playbackRate.value=e)}preload(){this.queueRef.onDebug(`preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`),this._actor.getSnapshot().value==="idle"&&this._actor.send({type:"PRELOAD"}),!this.audioBuffer&&(y(),this.ctx&&this._actor.send({type:"START_FETCH"}))}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e&&this.seek(t-e)}activate(){this._actor.send({type:"ACTIVATE"})}deactivate(){this.queueRef.onDebug(`Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`),this._notifiedPreloadThreshold=!1,this._actor.send({type:"DEACTIVATE"}),this.queueRef.onDebug(`Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`)}destroy(){var e;this.deactivate(),this._pendingArrayBuffer=null,this.audioBuffer=null,(e=this.gainNode)==null||e.disconnect(),this.gainNode=null,this._actor.stop()}cancelGaplessStart(){this._actor.getSnapshot().context.scheduledStartContextTime!==null&&this._actor.send({type:"CANCEL_GAPLESS"})}scheduleGaplessStart(e){!this.ctx||!this.audioBuffer||!this.gainNode||this._actor.send({type:"SCHEDULE_GAPLESS",when:e})}get currentTime(){let e=this._actor.getSnapshot();return e.value==="webaudio"?e.context.isPlaying?this.ctx?Math.max(0,this._waRefTrackTime+(this.ctx.currentTime-this._waRefCtxTime)*this.queueRef.playbackRate):0:this.pausedAtTrackTime:this.audio.currentTime}get duration(){return this._actor.getSnapshot().value==="html5"&&!isNaN(this.audio.duration)?this.audio.duration:this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isPaused(){let e=this._actor.getSnapshot();return e.value==="webaudio"?!e.context.isPlaying:this.audio.paused}get isPlaying(){return this._actor.getSnapshot().context.isPlaying}get trackUrl(){return this._resolvedUrl}get playbackType(){return this._actor.getSnapshot().context.playbackType}get webAudioLoadingState(){return this._actor.getSnapshot().context.webAudioLoadingState}get hasSourceNode(){return this.sourceNode!==null}get machineState(){return this._actor.getSnapshot().value}get scheduledStartContextTime(){return this._actor.getSnapshot().context.scheduledStartContextTime}get isBufferLoaded(){return this.audioBuffer!==null}toInfo(){var e;return{index:this.index,currentTime:this.currentTime,duration:this.duration,isPlaying:this.isPlaying,isPaused:this.isPaused,volume:((e=this.gainNode)==null?void 0:e.gain.value)??this.audio.volume,trackUrl:this.trackUrl,playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,metadata:this.metadata,playbackRate:this.queueRef.playbackRate,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.playbackRate=this.queueRef.playbackRate;let e=this.audio.play();e&&e.catch(t=>{t instanceof Error&&t.name==="NotAllowedError"?this.queueRef.onPlayBlocked():t instanceof Error&&t.name==="AbortError"||this.queueRef.onError(t instanceof Error?t:new Error(String(t)))})}_seekHtml5(){let e=this.pausedAtTrackTime;isFinite(e)&&(this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.readyState>=HTMLMediaElement.HAVE_METADATA?this.audio.currentTime=e:(this.audio.addEventListener("loadedmetadata",()=>{this.audio.currentTime=e},{once:!0}),this.audio.load()))}_crossoverHtml5ToWebAudio(e){if(!this.ctx||!this.audioBuffer||!this.gainNode)return;let t=this.audio.currentTime;if(this.pausedAtTrackTime=isFinite(t)?t:0,!e){this.audio.pause(),this.queueRef.onDebug(`crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=false`);return}let r=this.ctx.currentTime,a=r+A;if(this._html5GainNode){this._html5GainNode.gain.cancelScheduledValues(r),this._html5GainNode.gain.setValueAtTime(1,r),this._html5GainNode.gain.linearRampToValueAtTime(0,a);let s=this.ctx,d=this._html5GainNode;setTimeout(()=>{this.audio.pause(),s&&d&&(d.gain.cancelScheduledValues(s.currentTime),d.gain.setValueAtTime(1,s.currentTime))},A*1e3+5)}else{let s=this.audio.volume;this.audio.volume=0,this.audio.pause(),this.audio.volume=s}this._startSourceNode(this.pausedAtTrackTime,A),this.queueRef.onDebug(`crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=true fade=${A}s mediaSource=${!!this._html5GainNode}`)}_maybeComputeBufferAlignment(){this.audioBuffer&&(this._bufferAlignmentMeasured||(this._bufferStartPaddingSec=0,this._bufferAlignmentMeasured=!0,this.queueRef.onDebug(`_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 \u2014 see comment)`)))}_startSourceNode(e,t=0){if(!(!this.ctx||!this.audioBuffer||!this.gainNode)){if(this._maybeComputeBufferAlignment(),this._stopSourceNode(),this.ctx.state==="suspended"&&this.ctx.resume(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.playbackRate.value=this.queueRef.playbackRate,t>0){let r=this.ctx.createGain(),a=this.ctx.currentTime;r.gain.setValueAtTime(0,a),r.gain.linearRampToValueAtTime(1,a+t),this.sourceNode.connect(r),r.connect(this.gainNode)}else this.sourceNode.connect(this.gainNode);this.sourceNode.onended=this._handleWebAudioEnded,this._waRefCtxTime=this.ctx.currentTime,this._waRefTrackTime=e,this.sourceNode.start(0,e+this._bufferStartPaddingSec)}}_stopSourceNode(){if(this.sourceNode){this.sourceNode.onended=null;try{this.sourceNode.stop()}catch{}try{this.sourceNode.disconnect()}catch{}this.sourceNode=null}}_seekWebAudio(){let t=this._actor.getSnapshot().context.isPlaying,r=this.pausedAtTrackTime;this._stopSourceNode(),t&&this._startSourceNode(r)}_handleWebAudioEnded=()=>{this.queueRef.onDebug(`_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`),this.sourceNode&&this._actor.send({type:"WEBAUDIO_ENDED"})};startProgressLoop(){if(this.rafId!==null)return;let e=()=>{if(this.isPaused||!this.isPlaying){this.rafId=null;return}this.queueRef.onProgress(this.toInfo());let t=this.duration-this.currentTime;!this._actor.getSnapshot().context.notifiedLookahead&&!isNaN(t)&&t<=z&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this)));let a=isNaN(this.duration)?L:Math.min(this.duration*.2,L);!this._notifiedPreloadThreshold&&this.currentTime>=a&&(this._notifiedPreloadThreshold=!0,queueMicrotask(()=>this.queueRef.onPreloadReady(this))),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}_stopProgressLoop(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}};function I(o,e){let t=0;return(...r)=>{let a=performance.now();a-t<e||(t=a,o(...r))}}var M=5,_=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onDebug;playbackMethod;_volume;_preloadNumTracks;_playbackRate;_scheduledNextIndex=null;_throttledUpdatePositionState=I((e,t,r)=>R(e,t,r),1e3);constructor(e={}){let{tracks:t=[],onProgress:r,onEnded:a,onPlayNextTrack:s,onPlayPreviousTrack:d,onStartNewTrack:u,onError:U,onPlayBlocked:O,onDebug:w,playbackMethod:G="HYBRID",trackMetadata:H=[],volume:B=1,preloadNumTracks:F=2,playbackRate:$=1}=e;this._volume=Math.min(1,Math.max(0,B)),this._preloadNumTracks=Math.max(0,F),this._playbackRate=Math.min(4,Math.max(.25,$)),this.playbackMethod=G,this._onProgress=r,this._onEnded=a,this._onPlayNextTrack=s,this._onPlayPreviousTrack=d,this._onStartNewTrack=u,this._onError=U,this._onPlayBlocked=O,this._onDebug=w,this._tracks=t.map((n,i)=>new f({trackUrl:n,index:i,queue:this,metadata:H[i]}));let W=C({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.deactivate()},deactivateEndedTrack:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.deactivate()},activateAndPlayCurrent:({context:n})=>{let i=this._trackAt(n.currentTrackIndex);i&&(i.activate(),this._scheduledNextIndex!==i.index&&i.play())},playOrContinueGapless:({context:n})=>{let i=this._trackAt(n.currentTrackIndex);i&&(this._scheduledNextIndex!==i.index?i.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${i.index} \u2014 sourceNode=${i.hasSourceNode} isPlaying=${i.isPlaying} machineState=${i.machineState}`),i.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:n})=>{var l;let i=this._trackAt(n.currentTrackIndex);i&&((l=this._onStartNewTrack)==null||l.call(this,i.toInfo()))},notifyPlayNextTrack:({context:n})=>{var l;let i=this._trackAt(n.currentTrackIndex);i&&((l=this._onPlayNextTrack)==null||l.call(this,i.toInfo()))},notifyPlayPreviousTrack:({context:n})=>{var l;let i=this._trackAt(n.currentTrackIndex);i&&((l=this._onPlayPreviousTrack)==null||l.call(this,i.toInfo()))},notifyEnded:()=>{var n;return(n=this._onEnded)==null?void 0:n.call(this)},updateMediaSessionMetadata:({context:n})=>{let i=this._trackAt(n.currentTrackIndex);i&&b(i.metadata)},preloadAhead:({context:n})=>{this._preloadAhead(n.currentTrackIndex)},playCurrent:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.play()},pauseCurrent:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.pause()},seekCurrent:({context:n,event:i})=>{var v;let l=i;(v=this._trackAt(n.currentTrackIndex))==null||v.seek(l.time)},seekCurrentToZero:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.seek(0)},scheduleGapless:({context:n})=>{this._tryScheduleGapless(n.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:n})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(n.currentTrackIndex)}}});this._actor=j(W),this._actor.subscribe(n=>{N(n.value==="playing")}),this._actor.start(),P({onPlay:()=>this.play(),onPause:()=>{this._actor.getSnapshot().value==="playing"&&this.pause()},onNext:()=>this.next(),onPrevious:()=>this.previous(),onSeek:n=>this.seek(n)})}play(){this._currentTrack&&this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}togglePlayPause(){this._actor.getSnapshot().value==="playing"?this.pause():this.play()}next(){this._actor.getSnapshot().context.currentTrackIndex+1>=this._tracks.length||this._actor.send({type:"NEXT"})}previous(){let e=this._currentTrack;if(e&&e.currentTime>8){e.seek(0),e.play();return}this._actor.send({type:"PREVIOUS"})}gotoTrack(e,t=!1){e<0||e>=this._tracks.length||(this.onDebug(`gotoTrack(${e}, playImmediately=${t}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`),this._actor.send({type:"GOTO",index:e,playImmediately:t}))}seek(e){this._actor.send({type:"SEEK",time:e})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._volume=t;for(let r of this._tracks)r.setVolume(t)}setPlaybackRate(e){var a;let t=Math.min(4,Math.max(.25,e));this._playbackRate=t,(a=this._currentTrack)==null||a.setPlaybackRate(t),this._cancelScheduledGapless();let r=this._actor.getSnapshot();r.value==="playing"&&this._tryScheduleGapless(r.context.currentTrackIndex)}addTrack(e,t={}){let r=this._tracks.length,a=t.metadata??{};this._tracks.push(new f({trackUrl:e,index:r,queue:this,skipHEAD:t.skipHEAD,metadata:a})),this._actor.send({type:"ADD_TRACK"})}removeTrack(e){if(!(e<0||e>=this._tracks.length)){this._tracks[e].destroy(),this._tracks.splice(e,1);for(let t=e;t<this._tracks.length;t++)this._tracks[t].index=t;this._scheduledNextIndex===e?this._scheduledNextIndex=null:this._scheduledNextIndex!==null&&this._scheduledNextIndex>e&&this._scheduledNextIndex--,this._actor.send({type:"REMOVE_TRACK",index:e})}}resumeAudioContext(){return y()}destroy(){for(let e of this._tracks)e.destroy();this._tracks=[],this._actor.stop()}get currentTrack(){var e;return(e=this._currentTrack)==null?void 0:e.toInfo()}get currentTrackIndex(){return this._actor.getSnapshot().context.currentTrackIndex}get tracks(){return this._tracks.map(e=>e.toInfo())}get isPlaying(){return this._actor.getSnapshot().value==="playing"}get isPaused(){return this._actor.getSnapshot().value==="paused"}get volume(){return this._volume}get preloadNumTracks(){return this._preloadNumTracks}set preloadNumTracks(e){this._preloadNumTracks=Math.max(0,e);let t=this._actor.getSnapshot();t.value==="playing"&&this._preloadAhead(t.context.currentTrackIndex)}get playbackRate(){return this._playbackRate}get queueSnapshot(){let e=this._actor.getSnapshot();return{state:e.value,context:e.context}}onTrackEnded(e){let t=this._actor.getSnapshot();if(this.onDebug(`onTrackEnded track=${e.index} queueState=${t.value} curIdx=${t.context.currentTrackIndex}`),e!==this._trackAt(t.context.currentTrackIndex))return;this._actor.send({type:"TRACK_ENDED"});let r=this._actor.getSnapshot();this.onDebug(`onTrackEnded after TRACK_ENDED \u2192 queueState=${r.value} curIdx=${r.context.currentTrackIndex}`)}onTrackBufferReady(e){this._actor.send({type:"TRACK_LOADED",index:e.index})}onProgress(e){var t;e.index===this._actor.getSnapshot().context.currentTrackIndex&&(isNaN(e.duration)||this._throttledUpdatePositionState(e.duration,e.currentTime,this._playbackRate),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;this._actor.send({type:"PAUSE"}),(e=this._onPlayBlocked)==null||e.call(this)}onPreloadReady(e){let t=this._actor.getSnapshot();e.index===t.context.currentTrackIndex&&this._preloadAhead(t.context.currentTrackIndex)}onDebug(e){var t;(t=this._onDebug)==null||t.call(this,e)}_trackAt(e){return this._tracks[e]}get _currentTrack(){return this._tracks[this._actor.getSnapshot().context.currentTrackIndex]}_preloadAhead(e){let t=this._trackAt(e);if(t!=null&&t.isPlaying&&t.playbackType==="HTML5"&&t.webAudioLoadingState==="LOADING"){this.onDebug(`_preloadAhead(${e}): deferring all \u2014 current track buffer still loading`);return}let r=t!=null&&t.isPlaying&&(()=>{let s=isNaN(t.duration)?15:Math.min(t.duration*.2,15);return t.currentTime<s})(),a=e+this._preloadNumTracks+1;this.onDebug(`_preloadAhead(${e}) limit=${a} trackCount=${this._tracks.length} belowThreshold=${r}`);for(let s=e+1;s<this._tracks.length&&s<a;s++){let d=this._tracks[s];if(s>e+1&&r){this.onDebug(`_preloadAhead: deferring track ${s} (current at ${t.currentTime.toFixed(1)}s, below threshold)`);return}if(d.isBufferLoaded)this.onDebug(`_preloadAhead: track ${s} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${s}`),d.preload();return}}}_cancelScheduledGapless(){if(this._scheduledNextIndex===null)return;let e=this._trackAt(this._scheduledNextIndex);e&&(e.cancelGaplessStart(),this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`)),this._scheduledNextIndex=null}_tryScheduleGapless(e){let t=k();if(!t||this.playbackMethod==="HTML5_ONLY")return;let r=e+1;if(r>=this._tracks.length)return;let a=this._tracks[e],s=this._tracks[r];if(!a.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===r||!a.isPlaying)return;let d=this._computeTrackEndTime(a);if(d===null||d<t.currentTime+.01)return;let u=d-t.currentTime;if(a.playbackType==="HTML5"&&u>M){this.onDebug(`_tryScheduleGapless: deferring \u2014 HTML5 track ${e} has ${u.toFixed(1)}s remaining (max lookahead=${M}s)`);return}s.scheduleGaplessStart(d),this.onDebug(`_tryScheduleGapless: scheduled track ${r} at endTime=${d.toFixed(3)} (in ${(d-t.currentTime).toFixed(1)}s) curPlaybackType=${a.playbackType}`),this._scheduledNextIndex=r}_computeTrackEndTime(e){let t=k();if(!t||!e.isBufferLoaded)return null;let r=e.duration;if(isNaN(r))return null;if(e.scheduledStartContextTime!==null)return e.scheduledStartContextTime+r/this._playbackRate;let a=(r-e.currentTime)/this._playbackRate;return a<=0?null:t.currentTime+a}};export{_ as Queue,_ as default};
|
|
1
|
+
import{createActor as j}from"xstate";var q=typeof window<"u",p;function V(){if(!q)return null;let o=window.AudioContext??window.webkitAudioContext;return o?new o:null}function k(){return p===void 0?null:p}function y(){p===void 0&&(p=V());let o=p;return o&&o.state==="suspended"?o.resume():Promise.resolve()}var g=typeof navigator<"u"&&"mediaSession"in navigator;function P(o){if(!g)return;let{mediaSession:e}=navigator;e.setActionHandler("play",o.onPlay),e.setActionHandler("pause",o.onPause),e.setActionHandler("nexttrack",o.onNext),e.setActionHandler("previoustrack",o.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&o.onSeek(t.seekTime)})}function b(o){if(g){if(!o){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:o.title??"",artist:o.artist??"",album:o.album??"",artwork:o.artwork??[]})}}function N(o){g&&(navigator.mediaSession.playbackState=o?"playing":"paused")}function R(o,e,t=1){if(g)try{navigator.mediaSession.setPositionState({duration:o,position:e,playbackRate:t})}catch{}}import{setup as K,assign as h}from"xstate";function C(o){return K({types:{context:{},events:{}},guards:{hasNextTrack:({context:e})=>e.currentTrackIndex+1<e.trackCount,playImmediately:({event:e})=>!!e.playImmediately,willBeEmpty:({context:e})=>e.trackCount<=1,isRemovingCurrent:({context:e,event:t})=>t.index===e.currentTrackIndex},actions:{incrementTrackCount:h({trackCount:({context:e})=>e.trackCount+1}),decrementTrackCount:h({trackCount:({context:e})=>Math.max(0,e.trackCount-1),currentTrackIndex:({context:e,event:t})=>{let r=t,a=Math.max(0,e.trackCount-1);return r.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):a>0?Math.min(e.currentTrackIndex,a-1):0}}),gotoTrackIndex:h({currentTrackIndex:({event:e})=>e.index}),advanceToNextTrack:h({currentTrackIndex:({context:e})=>{let t=e.currentTrackIndex+1;return t<e.trackCount?t:e.currentTrackIndex}}),goToPreviousTrack:h({currentTrackIndex:({context:e})=>Math.max(0,e.currentTrackIndex-1)}),advanceOnTrackEnd:h({currentTrackIndex:({context:e})=>Math.min(e.currentTrackIndex+1,e.trackCount-1)}),resetToFirstTrack:h({currentTrackIndex:()=>0}),deactivateCurrent:()=>{},deactivateEndedTrack:()=>{},activateAndPlayCurrent:()=>{},playOrContinueGapless:()=>{},cancelAllGapless:()=>{},notifyStartNewTrack:()=>{},notifyPlayNextTrack:()=>{},notifyPlayPreviousTrack:()=>{},notifyEnded:()=>{},updateMediaSessionMetadata:()=>{},preloadAhead:()=>{},playCurrent:()=>{},pauseCurrent:()=>{},seekCurrent:()=>{},seekCurrentToZero:()=>{},scheduleGapless:()=>{},cancelScheduledGapless:()=>{},cancelAndRescheduleGapless:()=>{}}}).createMachine({id:"queue",initial:"idle",context:o,on:{ADD_TRACK:{actions:"incrementTrackCount"},REMOVE_TRACK:{actions:"decrementTrackCount"}},states:{idle:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},playing:{on:{REMOVE_TRACK:[{guard:"willBeEmpty",target:"idle",actions:["cancelAllGapless","decrementTrackCount"]},{guard:"isRemovingCurrent",actions:["cancelAllGapless","decrementTrackCount","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["decrementTrackCount","cancelAndRescheduleGapless","preloadAhead"]}],PAUSE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},TOGGLE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent","cancelAndRescheduleGapless"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"playing",actions:["deactivateEndedTrack","advanceOnTrackEnd","playOrContinueGapless","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:[{guard:({context:e,event:t})=>t.index===e.currentTrackIndex,actions:["cancelAndRescheduleGapless","preloadAhead"]},{actions:["scheduleGapless","preloadAhead"]}]}},paused:{on:{REMOVE_TRACK:[{guard:"willBeEmpty",target:"idle",actions:["cancelAllGapless","decrementTrackCount"]},{guard:"isRemovingCurrent",actions:["cancelAllGapless","decrementTrackCount","preloadAhead"]},{actions:["decrementTrackCount","preloadAhead"]}],PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},TOGGLE:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"paused",actions:["deactivateEndedTrack","advanceOnTrackEnd","notifyStartNewTrack","updateMediaSessionMetadata"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},ended:{on:{ADD_TRACK:{target:"paused",actions:"incrementTrackCount"},PLAY:{target:"playing",actions:["resetToFirstTrack","playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}]}}}})}import{createActor as X,fromPromise as x}from"xstate";import{setup as Q,assign as c,spawnChild as Z}from"xstate";import{setup as Y,assign as E,sendParent as T,fromPromise as S}from"xstate";var m=Y({types:{context:{},input:{}},actors:{resolveUrl:S(async()=>null),fetchAudio:S(async()=>{}),decodeAudio:S(async()=>{})},guards:{shouldSkipHEAD:({context:o})=>o.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:{trackUrl:"",resolvedUrl:"",skipHEAD:!1},entry:E(({event:o})=>{let{input:e}=o;return{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:o})=>({trackUrl:o.trackUrl}),onDone:{target:"fetching",actions:[E({resolvedUrl:({event:o,context:e})=>o.output??e.resolvedUrl,skipHEAD:()=>!0}),T(({event:o,context:e})=>({type:"URL_RESOLVED",url:o.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:E({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:o})=>({resolvedUrl:o.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:T({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:T({type:"BUFFER_READY"})},onError:{target:"error",actions:T({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function L(o){return Q({types:{context:{},events:{}},actors:{fetchDecode:m},guards:{canPlayWebAudio:()=>!1,isWebAudioOnly:()=>!1,canStartFetch:({context:e})=>e.webAudioLoadingState==="NONE"&&!e.fetchStarted},actions:{playHtml5:()=>{},startSourceNode:()=>{},crossoverHtml5ToWebAudio:()=>{},notifyBufferReady:()=>{},startScheduledSourceNode:()=>{},startProgressLoop:()=>{},pauseHtml5:()=>{},freezePausedTime:()=>{},stopSourceNode:()=>{},stopProgressLoop:()=>{},reportProgress:()=>{},seekHtml5:()=>{},seekWebAudio:()=>{},resetHtml5Element:()=>{},resetTiming:()=>{},notifyTrackEnded:()=>{},triggerFetchForPendingPlay:()=>{},setPendingPlay:c({pendingPlay:()=>!0}),clearPendingPlay:c({pendingPlay:()=>!1}),setIsPlaying:c({isPlaying:()=>!0}),clearIsPlaying:c({isPlaying:()=>!1}),setLoadingState:c({webAudioLoadingState:()=>"LOADING"}),setLoadedState:c({webAudioLoadingState:()=>"LOADED"}),setErrorState:c({webAudioLoadingState:()=>"ERROR"}),clearScheduleAndLookahead:c({scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setPlayingWebAudio:c({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO"}),setScheduledGapless:c({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO",scheduledStartContextTime:({event:e})=>e.when}),clearPlayingAndSchedule:c({isPlaying:()=>!1,scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setNotifiedLookahead:c({notifiedLookahead:()=>!0}),setResolvedUrl:c({resolvedUrl:({event:e})=>e.url}),clearScheduledStart:c({scheduledStartContextTime:()=>null}),setPlayingWebAudioType:c({isPlaying:()=>!0,playbackType:()=>"WEBAUDIO"}),setPlaybackTypeWebAudio:c({playbackType:()=>"WEBAUDIO"}),clearNotifiedLookahead:c({notifiedLookahead:()=>!1})}}).createMachine({id:"track",initial:"idle",context:o,on:{START_FETCH:{guard:"canStartFetch",actions:[c({webAudioLoadingState:()=>"LOADING",fetchStarted:()=>!0}),Z("fetchDecode",{id:"fetchDecode",input:({context:e})=>({trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD})})]}},states:{idle:{on:{HTML5_ENDED:{actions:["notifyTrackEnded"]},DEACTIVATE:{actions:["pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop","clearScheduleAndLookahead"]},ACTIVATE:{actions:["resetTiming","resetHtml5Element","clearScheduleAndLookahead"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{guard:"isWebAudioOnly",actions:["setPendingPlay","triggerFetchForPendingPlay"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop","triggerFetchForPendingPlay"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},PRELOAD:{target:"loading"},BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:[{guard:({context:e})=>e.pendingPlay,target:"webaudio",actions:["clearPendingPlay","setPlayingWebAudio","startSourceNode","startProgressLoop","notifyBufferReady"]},{actions:["setLoadedState","notifyBufferReady"]}],BUFFER_ERROR:{actions:["setErrorState","clearPendingPlay"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},html5:{on:{PAUSE:{actions:["clearIsPlaying","pauseHtml5","stopProgressLoop","reportProgress"]},PLAY:{actions:["setIsPlaying","playHtml5","startProgressLoop"]},BUFFER_LOADING:{actions:"setLoadingState"},PLAY_WEBAUDIO:{target:"webaudio",actions:"setPlayingWebAudio"},BUFFER_READY:{target:"webaudio",actions:["setLoadedState","crossoverHtml5ToWebAudio","setPlaybackTypeWebAudio","clearNotifiedLookahead","notifyBufferReady"]},BUFFER_ERROR:{actions:"setErrorState"},SEEK:{actions:["seekHtml5","reportProgress"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},HTML5_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","pauseHtml5","stopProgressLoop","resetTiming","resetHtml5Element"]},URL_RESOLVED:{actions:"setResolvedUrl"},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop"]}}},loading:{on:{BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:[{guard:({context:e})=>e.pendingPlay,target:"webaudio",actions:["clearPendingPlay","setPlayingWebAudio","startSourceNode","startProgressLoop","notifyBufferReady"]},{target:"idle",actions:["setLoadedState","notifyBufferReady"]}],BUFFER_ERROR:{target:"idle",actions:["setErrorState","clearPendingPlay"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{guard:"isWebAudioOnly",actions:["setPendingPlay","triggerFetchForPendingPlay"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop","triggerFetchForPendingPlay"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","resetTiming"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},webaudio:{on:{PAUSE:{actions:["clearIsPlaying","freezePausedTime","stopSourceNode","stopProgressLoop","reportProgress"]},PLAY:[{guard:"canPlayWebAudio",actions:["setIsPlaying","startSourceNode","startProgressLoop"]},{actions:"setIsPlaying"}],PLAY_WEBAUDIO:{actions:"setPlayingWebAudioType"},SEEK:{actions:["clearScheduledStart","seekWebAudio","reportProgress"]},SET_VOLUME:{},CANCEL_GAPLESS:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","stopProgressLoop","resetTiming"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},WEBAUDIO_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","stopProgressLoop","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","resetTiming","resetHtml5Element","stopProgressLoop"]}}}}})}var z=5,D=15,A=.03,f=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_playbackMethod;get ctx(){if(this._playbackMethod==="HTML5_ONLY")return null;let e=k();if(e&&!this.gainNode){this.gainNode=e.createGain(),this.gainNode.gain.value=this.audio.volume,this.gainNode.connect(e.destination);try{this._mediaElementSource=e.createMediaElementSource(this.audio),this._html5GainNode=e.createGain(),this._html5GainNode.gain.value=1,this._mediaElementSource.connect(this._html5GainNode),this._html5GainNode.connect(this.gainNode),this.audio.volume=1}catch{this._mediaElementSource=null,this._html5GainNode=null}}return e}gainNode=null;_mediaElementSource=null;_html5GainNode=null;sourceNode=null;audioBuffer=null;_bufferStartPaddingSec=0;_bufferAlignmentMeasured=!1;_waRefCtxTime=0;_waRefTrackTime=0;pausedAtTrackTime=0;_actor;queueRef;rafId=null;_notifiedPreloadThreshold=!1;constructor(e){this.index=e.index,this._trackUrl=e.trackUrl,this._resolvedUrl=e.trackUrl,this.skipHEAD=e.skipHEAD??!1,this.metadata=e.metadata??{},this.queueRef=e.queue,this.audio=new Audio,this.audio.preload="none",e.queue.playbackMethod!=="HTML5_ONLY"&&(this.audio.crossOrigin="anonymous"),this.audio.src=this._trackUrl,this.audio.volume=e.queue.volume,this.audio.controls=!1,this.audio.onerror=()=>{var d,u;let a=(d=this.audio.error)==null?void 0:d.code;if(a===1)return;let s=((u=this.audio.error)==null?void 0:u.message)??"unknown";this.queueRef.onError(new Error(`HTML5 audio error on track ${this.index} (code ${a}): ${s}`))},this.audio.onended=()=>{this.queueRef.onDebug(`audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`),this._actor.send({type:"HTML5_ENDED"})},this._playbackMethod=e.queue.playbackMethod;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchStarted:!1,pendingPlay:!1},r=L(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode),isWebAudioOnly:()=>this._playbackMethod==="WEBAUDIO_ONLY"},actors:{fetchDecode:m.provide({actors:{resolveUrl:x(async({signal:a})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:a});return s.redirected&&s.url?(this._resolvedUrl=s.url,s.url):null}),fetchAudio:x(async({input:a,signal:s})=>{let{resolvedUrl:d}=a,u=await fetch(d,{signal:s});if(!u.ok)throw new Error(`HTTP ${u.status} for ${d}`);this._pendingArrayBuffer=await u.arrayBuffer()}),decodeAudio:x(async()=>{let a=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!a||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(a),this._maybeComputeBufferAlignment()})}})},actions:{triggerFetchForPendingPlay:()=>{this.preload(),y()},playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},crossoverHtml5ToWebAudio:({context:a})=>{this._crossoverHtml5ToWebAudio(a.isPlaying)},startScheduledSourceNode:({context:a})=>{let s=a.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._maybeComputeBufferAlignment(),this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.playbackRate.value=this.queueRef.playbackRate,this.sourceNode.connect(this.gainNode),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,this._bufferStartPaddingSec),this._waRefCtxTime=s,this._waRefTrackTime=0,this.queueRef.onDebug(`startScheduledSourceNode track=${this.index} when=${s.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(s-this.ctx.currentTime).toFixed(3)}s padding=${this._bufferStartPaddingSec.toFixed(3)}s`))},startProgressLoop:()=>this.startProgressLoop(),pauseHtml5:()=>this.audio.pause(),freezePausedTime:()=>{let a=this.currentTime;this.pausedAtTrackTime=isFinite(a)?a:0},stopSourceNode:()=>this._stopSourceNode(),stopProgressLoop:()=>this._stopProgressLoop(),reportProgress:()=>{queueMicrotask(()=>this.queueRef.onProgress(this.toInfo()))},seekHtml5:()=>this._seekHtml5(),seekWebAudio:()=>this._seekWebAudio(),resetHtml5Element:()=>{this.audio.currentTime=0},resetTiming:()=>{this._waRefCtxTime=0,this._waRefTrackTime=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))},notifyBufferReady:()=>{queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))}}});this._actor=X(r),this._actor.start()}play(){this.queueRef.onDebug(`Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`),this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}seek(e){if(!isFinite(e))return;let t=Math.max(0,isNaN(this.duration)?e:Math.min(e,this.duration));this.pausedAtTrackTime=t,this._actor.send({type:"SEEK",time:t})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._mediaElementSource?this.audio.volume=1:this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t),this._actor.send({type:"SET_VOLUME",volume:t})}setPlaybackRate(e){if(this.ctx&&this.sourceNode&&this._actor.getSnapshot().context.isPlaying){let t=this.sourceNode.playbackRate.value;this._waRefTrackTime=this._waRefTrackTime+(this.ctx.currentTime-this._waRefCtxTime)*t,this._waRefCtxTime=this.ctx.currentTime}this.audio.playbackRate=e,this.sourceNode&&(this.sourceNode.playbackRate.value=e)}preload(){this.queueRef.onDebug(`preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`),this._actor.getSnapshot().value==="idle"&&this._actor.send({type:"PRELOAD"}),!this.audioBuffer&&(y(),this.ctx&&this._actor.send({type:"START_FETCH"}))}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e&&this.seek(t-e)}activate(){this._actor.send({type:"ACTIVATE"})}deactivate(){this.queueRef.onDebug(`Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`),this._notifiedPreloadThreshold=!1,this._actor.send({type:"DEACTIVATE"}),this.queueRef.onDebug(`Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`)}destroy(){var e;this.deactivate(),this._pendingArrayBuffer=null,this.audioBuffer=null,(e=this.gainNode)==null||e.disconnect(),this.gainNode=null,this._actor.stop()}cancelGaplessStart(){this._actor.getSnapshot().context.scheduledStartContextTime!==null&&this._actor.send({type:"CANCEL_GAPLESS"})}scheduleGaplessStart(e){!this.ctx||!this.audioBuffer||!this.gainNode||this._actor.send({type:"SCHEDULE_GAPLESS",when:e})}get currentTime(){let e=this._actor.getSnapshot();return e.value==="webaudio"?e.context.isPlaying?this.ctx?Math.max(0,this._waRefTrackTime+(this.ctx.currentTime-this._waRefCtxTime)*this.queueRef.playbackRate):0:this.pausedAtTrackTime:this.audio.currentTime}get duration(){return this._actor.getSnapshot().value==="html5"&&!isNaN(this.audio.duration)?this.audio.duration:this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isPaused(){let e=this._actor.getSnapshot();return e.value==="webaudio"?!e.context.isPlaying:this.audio.paused}get isPlaying(){return this._actor.getSnapshot().context.isPlaying}get trackUrl(){return this._resolvedUrl}get playbackType(){return this._actor.getSnapshot().context.playbackType}get webAudioLoadingState(){return this._actor.getSnapshot().context.webAudioLoadingState}get hasSourceNode(){return this.sourceNode!==null}get machineState(){return this._actor.getSnapshot().value}get scheduledStartContextTime(){return this._actor.getSnapshot().context.scheduledStartContextTime}get isBufferLoaded(){return this.audioBuffer!==null}toInfo(){var e;return{index:this.index,currentTime:this.currentTime,duration:this.duration,isPlaying:this.isPlaying,isPaused:this.isPaused,volume:((e=this.gainNode)==null?void 0:e.gain.value)??this.audio.volume,trackUrl:this.trackUrl,playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,metadata:this.metadata,playbackRate:this.queueRef.playbackRate,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.playbackRate=this.queueRef.playbackRate;let e=this.audio.play();e&&e.catch(t=>{t instanceof Error&&t.name==="NotAllowedError"?this.queueRef.onPlayBlocked():t instanceof Error&&t.name==="AbortError"||this.queueRef.onError(t instanceof Error?t:new Error(String(t)))})}_seekHtml5(){let e=this.pausedAtTrackTime;isFinite(e)&&(this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.readyState>=HTMLMediaElement.HAVE_METADATA?this.audio.currentTime=e:(this.audio.addEventListener("loadedmetadata",()=>{this.audio.currentTime=e},{once:!0}),this.audio.load()))}_crossoverHtml5ToWebAudio(e){if(!this.ctx||!this.audioBuffer||!this.gainNode)return;let t=this.audio.currentTime;if(this.pausedAtTrackTime=isFinite(t)?t:0,!e){this.audio.pause(),this.queueRef.onDebug(`crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=false`);return}let r=this.ctx.currentTime,a=r+A;if(this._html5GainNode){this._html5GainNode.gain.cancelScheduledValues(r),this._html5GainNode.gain.setValueAtTime(1,r),this._html5GainNode.gain.linearRampToValueAtTime(0,a);let s=this.ctx,d=this._html5GainNode;setTimeout(()=>{this.audio.pause(),s&&d&&(d.gain.cancelScheduledValues(s.currentTime),d.gain.setValueAtTime(1,s.currentTime))},A*1e3+5)}else{let s=this.audio.volume;this.audio.volume=0,this.audio.pause(),this.audio.volume=s}this._startSourceNode(this.pausedAtTrackTime,A),this.queueRef.onDebug(`crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=true fade=${A}s mediaSource=${!!this._html5GainNode}`)}_maybeComputeBufferAlignment(){this.audioBuffer&&(this._bufferAlignmentMeasured||(this._bufferStartPaddingSec=0,this._bufferAlignmentMeasured=!0,this.queueRef.onDebug(`_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 \u2014 see comment)`)))}_startSourceNode(e,t=0){if(!(!this.ctx||!this.audioBuffer||!this.gainNode)){if(this._maybeComputeBufferAlignment(),this._stopSourceNode(),this.ctx.state==="suspended"&&this.ctx.resume(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.playbackRate.value=this.queueRef.playbackRate,t>0){let r=this.ctx.createGain(),a=this.ctx.currentTime;r.gain.setValueAtTime(0,a),r.gain.linearRampToValueAtTime(1,a+t),this.sourceNode.connect(r),r.connect(this.gainNode)}else this.sourceNode.connect(this.gainNode);this.sourceNode.onended=this._handleWebAudioEnded,this._waRefCtxTime=this.ctx.currentTime,this._waRefTrackTime=e,this.sourceNode.start(0,e+this._bufferStartPaddingSec)}}_stopSourceNode(){if(this.sourceNode){this.sourceNode.onended=null;try{this.sourceNode.stop()}catch{}try{this.sourceNode.disconnect()}catch{}this.sourceNode=null}}_seekWebAudio(){let t=this._actor.getSnapshot().context.isPlaying,r=this.pausedAtTrackTime;this._stopSourceNode(),t&&this._startSourceNode(r)}_handleWebAudioEnded=()=>{this.queueRef.onDebug(`_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`),this.sourceNode&&this._actor.send({type:"WEBAUDIO_ENDED"})};startProgressLoop(){if(this.rafId!==null)return;let e=()=>{if(this.isPaused||!this.isPlaying){this.rafId=null;return}this.queueRef.onProgress(this.toInfo());let t=this.duration-this.currentTime;!this._actor.getSnapshot().context.notifiedLookahead&&!isNaN(t)&&t<=z&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this)));let a=isNaN(this.duration)?D:Math.min(this.duration*.2,D);!this._notifiedPreloadThreshold&&this.currentTime>=a&&(this._notifiedPreloadThreshold=!0,queueMicrotask(()=>this.queueRef.onPreloadReady(this))),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}_stopProgressLoop(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}};function I(o,e){let t=0;return(...r)=>{let a=performance.now();a-t<e||(t=a,o(...r))}}var M=5,_=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onDebug;playbackMethod;_volume;_preloadNumTracks;_playbackRate;_scheduledNextIndex=null;_throttledUpdatePositionState=I((e,t,r)=>R(e,t,r),1e3);constructor(e={}){let{tracks:t=[],onProgress:r,onEnded:a,onPlayNextTrack:s,onPlayPreviousTrack:d,onStartNewTrack:u,onError:O,onPlayBlocked:U,onDebug:w,playbackMethod:G="HYBRID",trackMetadata:H=[],volume:B=1,preloadNumTracks:F=2,playbackRate:$=1}=e;this._volume=Math.min(1,Math.max(0,B)),this._preloadNumTracks=Math.max(0,F),this._playbackRate=Math.min(4,Math.max(.25,$)),this.playbackMethod=G,this._onProgress=r,this._onEnded=a,this._onPlayNextTrack=s,this._onPlayPreviousTrack=d,this._onStartNewTrack=u,this._onError=O,this._onPlayBlocked=U,this._onDebug=w,this._tracks=t.map((n,i)=>new f({trackUrl:n,index:i,queue:this,metadata:H[i]}));let W=C({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.deactivate()},deactivateEndedTrack:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.deactivate()},activateAndPlayCurrent:({context:n})=>{let i=this._trackAt(n.currentTrackIndex);i&&(i.activate(),this._scheduledNextIndex!==i.index&&i.play())},playOrContinueGapless:({context:n})=>{let i=this._trackAt(n.currentTrackIndex);i&&(this._scheduledNextIndex!==i.index?i.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${i.index} \u2014 sourceNode=${i.hasSourceNode} isPlaying=${i.isPlaying} machineState=${i.machineState}`),i.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:n})=>{var l;let i=this._trackAt(n.currentTrackIndex);i&&((l=this._onStartNewTrack)==null||l.call(this,i.toInfo()))},notifyPlayNextTrack:({context:n})=>{var l;let i=this._trackAt(n.currentTrackIndex);i&&((l=this._onPlayNextTrack)==null||l.call(this,i.toInfo()))},notifyPlayPreviousTrack:({context:n})=>{var l;let i=this._trackAt(n.currentTrackIndex);i&&((l=this._onPlayPreviousTrack)==null||l.call(this,i.toInfo()))},notifyEnded:()=>{var n;return(n=this._onEnded)==null?void 0:n.call(this)},updateMediaSessionMetadata:({context:n})=>{let i=this._trackAt(n.currentTrackIndex);i&&b(i.metadata)},preloadAhead:({context:n})=>{this._preloadAhead(n.currentTrackIndex)},playCurrent:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.play()},pauseCurrent:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.pause()},seekCurrent:({context:n,event:i})=>{var v;let l=i;(v=this._trackAt(n.currentTrackIndex))==null||v.seek(l.time)},seekCurrentToZero:({context:n})=>{var i;(i=this._trackAt(n.currentTrackIndex))==null||i.seek(0)},scheduleGapless:({context:n})=>{this._tryScheduleGapless(n.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:n})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(n.currentTrackIndex)}}});this._actor=j(W),this._actor.subscribe(n=>{N(n.value==="playing")}),this._actor.start(),P({onPlay:()=>this.play(),onPause:()=>{this._actor.getSnapshot().value==="playing"&&this.pause()},onNext:()=>this.next(),onPrevious:()=>this.previous(),onSeek:n=>this.seek(n)})}play(){this._currentTrack&&this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}togglePlayPause(){this._actor.getSnapshot().value==="playing"?this.pause():this.play()}next(){this._actor.getSnapshot().context.currentTrackIndex+1>=this._tracks.length||this._actor.send({type:"NEXT"})}previous(){let e=this._currentTrack;if(e&&e.currentTime>8){e.seek(0),e.play();return}this._actor.send({type:"PREVIOUS"})}gotoTrack(e,t=!1){e<0||e>=this._tracks.length||(this.onDebug(`gotoTrack(${e}, playImmediately=${t}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`),this._actor.send({type:"GOTO",index:e,playImmediately:t}))}seek(e){this._actor.send({type:"SEEK",time:e})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._volume=t;for(let r of this._tracks)r.setVolume(t)}setPlaybackRate(e){var a;let t=Math.min(4,Math.max(.25,e));this._playbackRate=t,(a=this._currentTrack)==null||a.setPlaybackRate(t),this._cancelScheduledGapless();let r=this._actor.getSnapshot();r.value==="playing"&&this._tryScheduleGapless(r.context.currentTrackIndex)}addTrack(e,t={}){let r=this._tracks.length,a=t.metadata??{};this._tracks.push(new f({trackUrl:e,index:r,queue:this,skipHEAD:t.skipHEAD,metadata:a})),this._actor.send({type:"ADD_TRACK"})}removeTrack(e){if(!(e<0||e>=this._tracks.length)){this._tracks[e].destroy(),this._tracks.splice(e,1);for(let t=e;t<this._tracks.length;t++)this._tracks[t].index=t;this._scheduledNextIndex===e?this._scheduledNextIndex=null:this._scheduledNextIndex!==null&&this._scheduledNextIndex>e&&this._scheduledNextIndex--,this._actor.send({type:"REMOVE_TRACK",index:e})}}resumeAudioContext(){return y()}destroy(){for(let e of this._tracks)e.destroy();this._tracks=[],this._actor.stop()}get currentTrack(){var e;return(e=this._currentTrack)==null?void 0:e.toInfo()}get currentTrackIndex(){return this._actor.getSnapshot().context.currentTrackIndex}get tracks(){return this._tracks.map(e=>e.toInfo())}get isPlaying(){return this._actor.getSnapshot().value==="playing"}get isPaused(){return this._actor.getSnapshot().value==="paused"}get volume(){return this._volume}get preloadNumTracks(){return this._preloadNumTracks}set preloadNumTracks(e){this._preloadNumTracks=Math.max(0,e);let t=this._actor.getSnapshot();t.value==="playing"&&this._preloadAhead(t.context.currentTrackIndex)}get playbackRate(){return this._playbackRate}get queueSnapshot(){let e=this._actor.getSnapshot();return{state:e.value,context:e.context}}onTrackEnded(e){let t=this._actor.getSnapshot();if(this.onDebug(`onTrackEnded track=${e.index} queueState=${t.value} curIdx=${t.context.currentTrackIndex}`),e!==this._trackAt(t.context.currentTrackIndex))return;this._actor.send({type:"TRACK_ENDED"});let r=this._actor.getSnapshot();this.onDebug(`onTrackEnded after TRACK_ENDED \u2192 queueState=${r.value} curIdx=${r.context.currentTrackIndex}`)}onTrackBufferReady(e){this._actor.send({type:"TRACK_LOADED",index:e.index})}onProgress(e){var t;e.index===this._actor.getSnapshot().context.currentTrackIndex&&(isNaN(e.duration)||this._throttledUpdatePositionState(e.duration,e.currentTime,this._playbackRate),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;this._actor.send({type:"PAUSE"}),(e=this._onPlayBlocked)==null||e.call(this)}onPreloadReady(e){let t=this._actor.getSnapshot();e.index===t.context.currentTrackIndex&&this._preloadAhead(t.context.currentTrackIndex)}onDebug(e){var t;(t=this._onDebug)==null||t.call(this,e)}_trackAt(e){return this._tracks[e]}get _currentTrack(){return this._tracks[this._actor.getSnapshot().context.currentTrackIndex]}_preloadAhead(e){let t=this._trackAt(e);if(t!=null&&t.isPlaying&&t.playbackType==="HTML5"&&t.webAudioLoadingState==="LOADING"){this.onDebug(`_preloadAhead(${e}): deferring all \u2014 current track buffer still loading`);return}let r=t!=null&&t.isPlaying&&(()=>{let s=isNaN(t.duration)?15:Math.min(t.duration*.2,15);return t.currentTime<s})(),a=e+this._preloadNumTracks+1;this.onDebug(`_preloadAhead(${e}) limit=${a} trackCount=${this._tracks.length} belowThreshold=${r}`);for(let s=e+1;s<this._tracks.length&&s<a;s++){let d=this._tracks[s];if(s>e+1&&r){this.onDebug(`_preloadAhead: deferring track ${s} (current at ${t.currentTime.toFixed(1)}s, below threshold)`);return}if(d.isBufferLoaded)this.onDebug(`_preloadAhead: track ${s} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${s}`),d.preload();return}}}_cancelScheduledGapless(){if(this._scheduledNextIndex===null)return;let e=this._trackAt(this._scheduledNextIndex);e&&(e.cancelGaplessStart(),this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`)),this._scheduledNextIndex=null}_tryScheduleGapless(e){let t=k();if(!t||this.playbackMethod==="HTML5_ONLY")return;let r=e+1;if(r>=this._tracks.length)return;let a=this._tracks[e],s=this._tracks[r];if(!a.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===r||!a.isPlaying)return;let d=this._computeTrackEndTime(a);if(d===null||d<t.currentTime+.01)return;let u=d-t.currentTime;if(a.playbackType==="HTML5"&&u>M){this.onDebug(`_tryScheduleGapless: deferring \u2014 HTML5 track ${e} has ${u.toFixed(1)}s remaining (max lookahead=${M}s)`);return}s.scheduleGaplessStart(d),this.onDebug(`_tryScheduleGapless: scheduled track ${r} at endTime=${d.toFixed(3)} (in ${(d-t.currentTime).toFixed(1)}s) curPlaybackType=${a.playbackType}`),this._scheduledNextIndex=r}_computeTrackEndTime(e){let t=k();if(!t||!e.isBufferLoaded)return null;let r=e.duration;if(isNaN(r))return null;if(e.scheduledStartContextTime!==null)return e.scheduledStartContextTime+r/this._playbackRate;let a=(r-e.currentTime)/this._playbackRate;return a<=0?null:t.currentTime+a}};export{_ as Queue,_ as default};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
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\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 /** 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 updateMediaSessionPlaybackState(snapshot.value === 'playing');\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 ct.seek(0);\n ct.play();\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 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 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 next.scheduleGaplessStart(endTime);\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 const duration = track.duration;\n if (isNaN(duration)) return null;\n\n if (track.scheduledStartContextTime !== null) {\n return track.scheduledStartContextTime + duration / this._playbackRate;\n }\n\n const remaining = (duration - track.currentTime) / this._playbackRate;\n if (remaining <= 0) return null;\n return ctx.currentTime + remaining;\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 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 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 if (this.ctx && this.sourceNode && this._actor.getSnapshot().context.isPlaying) {\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) this.sourceNode.playbackRate.value = rate;\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 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): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._actor.send({ type: 'SCHEDULE_GAPLESS', when });\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 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 // Sample-accurate crossfade. Both sides live on the AudioContext clock:\n // • HTML5 path: mediaElementSource → _html5GainNode → gainNode → destination\n // • WebAudio path: source → fadeGain → gainNode → destination\n //\n // We schedule a 1→0 ramp on _html5GainNode and a 0→1 ramp on the WebAudio\n // fadeGain, both over CROSSOVER_FADE_SEC and both starting at the same\n // ctx-clock time. The two streams overlap with constant-summed gain\n // through the fade window, smoothing out the sample-level discontinuity\n // that an instant cut would otherwise leave audible as a \"blip\".\n //\n // If _html5GainNode wasn't created (createMediaElementSource fell back —\n // e.g. CORS blocked or legacy browser), we degrade to immediate pause +\n // WebAudio fade-in only.\n const t0 = this.ctx.currentTime;\n const t1 = t0 + CROSSOVER_FADE_SEC;\n\n if (this._html5GainNode) {\n this._html5GainNode.gain.cancelScheduledValues(t0);\n this._html5GainNode.gain.setValueAtTime(1, t0);\n this._html5GainNode.gain.linearRampToValueAtTime(0, t1);\n // Pause the HTML5 element after the fade completes — it stops consuming\n // network/decoder resources and the gain is back to silent regardless.\n // Reset the gain to 1 afterwards so future plays through this element\n // (post-deactivate/reactivate) start at full level.\n const ctxRef = this.ctx;\n const html5GainRef = this._html5GainNode;\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 }, CROSSOVER_FADE_SEC * 1000 + 5);\n } else {\n // Fallback: no MediaElementSource path. Silence HTML5 immediately.\n const savedVolume = this.audio.volume;\n this.audio.volume = 0;\n this.audio.pause();\n this.audio.volume = savedVolume;\n }\n\n this._startSourceNode(this.pausedAtTrackTime, CROSSOVER_FADE_SEC);\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}`\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): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n // Re-check alignment in case audio.duration became available since the\n // last computation (e.g., preloaded track whose metadata load completed).\n this._maybeComputeBufferAlignment();\n this._stopSourceNode();\n\n // Ensure the AudioContext is running (it may have been suspended after\n // the previous track's source was disconnected).\n if (this.ctx.state === 'suspended') {\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 // When fadeInSec > 0 (crossover path), insert a per-source fade gain\n // between the source and the master gainNode. This gain ramps 0 → 1 over\n // fadeInSec so the new Web Audio output rises in step with the HTML5\n // element's audio-pipeline tail decaying. The master gainNode (which\n // represents user volume) is unaffected.\n if (fadeInSec > 0) {\n const fadeNode = this.ctx.createGain();\n const t0 = this.ctx.currentTime;\n fadeNode.gain.setValueAtTime(0, t0);\n fadeNode.gain.linearRampToValueAtTime(1, t0 + fadeInSec);\n this.sourceNode.connect(fadeNode);\n fadeNode.connect(this.gainNode);\n } else {\n this.sourceNode.connect(this.gainNode);\n }\n // gainNode → destination connected once at ctx setup; do not re-connect.\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this._waRefCtxTime = this.ctx.currentTime;\n this._waRefTrackTime = offset;\n // Buffer-internal offset = music-time offset + decoded-buffer start padding.\n // See _bufferStartPaddingSec docs for why this is necessary on MP3 files\n // with ID3v2 tags or encoder priming samples.\n this.sourceNode.start(0, offset + this._bufferStartPaddingSec);\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 ],\n },\n ACTIVATE: {\n actions: [\n 'resetTiming',\n 'resetHtml5Element',\n 'clearScheduleAndLookahead',\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 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', '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 PAUSE: {\n actions: [\n 'clearIsPlaying',\n 'freezePausedTime',\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","t","createActor","time","clamped","v","vol","rate","oldRate","secondsFromEnd","dur","snap","promise","err","wasPlaying","offset","t0","t1","ctxRef","html5GainRef","savedVolume","fadeInSec","fadeNode","loop","remaining","preloadThreshold","throttle","fn","ms","last","args","now","MAX_SCHEDULE_LOOKAHEAD","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","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","rate","snap","metadata","i","resumeAudioContext","value","newSnap","info","error","msg","fromIndex","curBelowThreshold","threshold","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","timeUntilEnd","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\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 /** 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 updateMediaSessionPlaybackState(snapshot.value === 'playing');\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 ct.seek(0);\n ct.play();\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 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 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 next.scheduleGaplessStart(endTime);\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 const duration = track.duration;\n if (isNaN(duration)) return null;\n\n if (track.scheduledStartContextTime !== null) {\n return track.scheduledStartContextTime + duration / this._playbackRate;\n }\n\n const remaining = (duration - track.currentTime) / this._playbackRate;\n if (remaining <= 0) return null;\n return ctx.currentTime + remaining;\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 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 if (this.ctx && this.sourceNode && this._actor.getSnapshot().context.isPlaying) {\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) this.sourceNode.playbackRate.value = rate;\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 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): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._actor.send({ type: 'SCHEDULE_GAPLESS', when });\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 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 // Sample-accurate crossfade. Both sides live on the AudioContext clock:\n // • HTML5 path: mediaElementSource → _html5GainNode → gainNode → destination\n // • WebAudio path: source → fadeGain → gainNode → destination\n //\n // We schedule a 1→0 ramp on _html5GainNode and a 0→1 ramp on the WebAudio\n // fadeGain, both over CROSSOVER_FADE_SEC and both starting at the same\n // ctx-clock time. The two streams overlap with constant-summed gain\n // through the fade window, smoothing out the sample-level discontinuity\n // that an instant cut would otherwise leave audible as a \"blip\".\n //\n // If _html5GainNode wasn't created (createMediaElementSource fell back —\n // e.g. CORS blocked or legacy browser), we degrade to immediate pause +\n // WebAudio fade-in only.\n const t0 = this.ctx.currentTime;\n const t1 = t0 + CROSSOVER_FADE_SEC;\n\n if (this._html5GainNode) {\n this._html5GainNode.gain.cancelScheduledValues(t0);\n this._html5GainNode.gain.setValueAtTime(1, t0);\n this._html5GainNode.gain.linearRampToValueAtTime(0, t1);\n // Pause the HTML5 element after the fade completes — it stops consuming\n // network/decoder resources and the gain is back to silent regardless.\n // Reset the gain to 1 afterwards so future plays through this element\n // (post-deactivate/reactivate) start at full level.\n const ctxRef = this.ctx;\n const html5GainRef = this._html5GainNode;\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 }, CROSSOVER_FADE_SEC * 1000 + 5);\n } else {\n // Fallback: no MediaElementSource path. Silence HTML5 immediately.\n const savedVolume = this.audio.volume;\n this.audio.volume = 0;\n this.audio.pause();\n this.audio.volume = savedVolume;\n }\n\n this._startSourceNode(this.pausedAtTrackTime, CROSSOVER_FADE_SEC);\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}`\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): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n // Re-check alignment in case audio.duration became available since the\n // last computation (e.g., preloaded track whose metadata load completed).\n this._maybeComputeBufferAlignment();\n this._stopSourceNode();\n\n // Ensure the AudioContext is running (it may have been suspended after\n // the previous track's source was disconnected).\n if (this.ctx.state === 'suspended') {\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 // When fadeInSec > 0 (crossover path), insert a per-source fade gain\n // between the source and the master gainNode. This gain ramps 0 → 1 over\n // fadeInSec so the new Web Audio output rises in step with the HTML5\n // element's audio-pipeline tail decaying. The master gainNode (which\n // represents user volume) is unaffected.\n if (fadeInSec > 0) {\n const fadeNode = this.ctx.createGain();\n const t0 = this.ctx.currentTime;\n fadeNode.gain.setValueAtTime(0, t0);\n fadeNode.gain.linearRampToValueAtTime(1, t0 + fadeInSec);\n this.sourceNode.connect(fadeNode);\n fadeNode.connect(this.gainNode);\n } else {\n this.sourceNode.connect(this.gainNode);\n }\n // gainNode → destination connected once at ctx setup; do not re-connect.\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this._waRefCtxTime = this.ctx.currentTime;\n this._waRefTrackTime = offset;\n // Buffer-internal offset = music-time offset + decoded-buffer start padding.\n // See _bufferStartPaddingSec docs for why this is necessary on MP3 files\n // with ID3v2 tags or encoder priming samples.\n this.sourceNode.start(0, offset + this._bufferStartPaddingSec);\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 ],\n },\n ACTIVATE: {\n actions: [\n 'resetTiming',\n 'resetHtml5Element',\n 'clearScheduleAndLookahead',\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 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', '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 PAUSE: {\n actions: [\n 'clearIsPlaying',\n 'freezePausedTime',\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,EACA,CACE,QAAS,CACP,oBACA,mBACA,iBACA,oBACA,cACF,CACF,CACF,EACA,KAAM,CACJ,QAAS,CAAC,aAAa,CACzB,EACA,YAAa,CACX,CACE,MAAO,eACP,OAAQ,SACR,QAAS,CACP,uBACA,oBACA,sBACA,4BACF,CACF,EACA,CACE,OAAQ,QACR,QAAS,CAAC,uBAAwB,aAAa,CACjD,CACF,EACA,aAAc,CACZ,QAAS,CAAC,cAAc,CAC1B,CACF,CACF,EAKA,MAAO,CACL,GAAI,CACF,UAAW,CACT,OAAQ,SACR,QAAS,qBACX,EACA,KAAM,CACJ,OAAQ,UACR,QAAS,CACP,oBACA,cACA,6BACA,eACA,iBACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,UACR,QAAS,CACP,oBACA,mBACA,iBACA,yBACA,sBACA,6BACA,cACF,CACF,EACA,CACE,OAAQ,SACR,QAAS,CACP,oBACA,mBACA,iBACA,oBACA,cACF,CACF,CACF,CACF,CACF,CACF,CACF,CAAC,CACH,CCxbA,OAAS,eAAAK,EAAa,eAAAC,MAAmB,SC2BzC,OAAS,SAAAC,EAAO,UAAAC,EAAQ,cAAAC,MAAkB,SCjB1C,OAAS,SAAAC,EAAO,UAAAC,EAAQ,cAAAC,EAAY,eAAAC,MAAmB,SAYhD,IAAMC,EAAqBJ,EAAM,CACtC,MAAO,CACL,QAAS,CAAC,EACV,MAAO,CAAC,CACV,EACA,OAAQ,CACN,WAAYG,EAAiD,SAAY,IAAI,EAC7E,WAAYA,EAA2C,SAAY,CAAC,CAAC,EACrE,YAAaA,EAAwB,SAAY,CAAC,CAAC,CACrD,EACA,OAAQ,CACN,eAAgB,CAAC,CAAE,QAAAE,CAAQ,IAAMA,EAAQ,QAC3C,CACF,CAAC,EAAE,cAAc,CACf,GAAI,cACJ,QAAS,eACT,QAAS,CACP,SAAU,GACV,YAAa,GACb,SAAU,EACZ,EACA,MAAOJ,EAAO,CAAC,CAAE,MAAAK,CAAM,IAAM,CAC3B,GAAM,CAAE,MAAAC,CAAM,EAAID,EAClB,MAAO,CACL,SAAUC,EAAM,SAChB,YAAaA,EAAM,YACnB,SAAUA,EAAM,QAClB,CACF,CAAC,EAED,OAAQ,CAIN,aAAc,CACZ,OAAQ,CACN,MAAO,iBACP,OAAQ,UACV,EACA,OAAQ,CACN,GAAI,aACJ,IAAK,aACL,MAAO,CAAC,CAAE,QAAAF,CAAQ,KAAO,CAAE,SAAUA,EAAQ,QAAS,GACtD,OAAQ,CACN,OAAQ,WACR,QAAS,CACPJ,EAAO,CACL,YAAa,CAAC,CAAE,MAAAK,EAAO,QAAAD,CAAQ,IAAMC,EAAM,QAAUD,EAAQ,YAC7D,SAAU,IAAM,EAClB,CAAC,EACDH,EAAW,CAAC,CAAE,MAAAI,EAAO,QAAAD,CAAQ,KAEpB,CAAE,KAAM,eAAyB,IAD5BC,EAAM,QACkCD,EAAQ,WAAY,EACzE,CACH,CACF,EACA,QAAS,CAEP,OAAQ,WACR,QAASJ,EAAO,CAAE,SAAU,IAAM,EAAK,CAAC,CAC1C,CACF,CACF,EAKA,SAAU,CACR,OAAQ,CACN,GAAI,aACJ,IAAK,aACL,MAAO,CAAC,CAAE,QAAAI,CAAQ,KAAO,CAAE,YAAaA,EAAQ,WAAY,GAC5D,OAAQ,WACR,QAAS,CACP,OAAQ,QACR,QAASH,EAAW,CAAE,KAAM,cAAe,CAAC,CAC9C,CACF,CACF,EAKA,SAAU,CACR,OAAQ,CACN,GAAI,cACJ,IAAK,cACL,MAAO,IAAM,CAAC,EACd,OAAQ,CACN,OAAQ,OACR,QAASA,EAAW,CAAE,KAAM,cAAe,CAAC,CAC9C,EACA,QAAS,CACP,OAAQ,QACR,QAASA,EAAW,CAAE,KAAM,cAAe,CAAC,CAC9C,CACF,CACF,EAKA,KAAM,CAAE,KAAM,OAAQ,EACtB,MAAO,CAAE,KAAM,OAAQ,CACzB,CACF,CAAC,EDxDM,SAASM,EAAmBC,EAA8B,CAC/D,OAAOC,EAAM,CACX,MAAO,CACL,QAAS,CAAC,EACV,OAAQ,CAAC,CACX,EACA,OAAQ,CACN,YAAaC,CACf,EACA,OAAQ,CACN,gBAAiB,IAAM,GACvB,eAAgB,IAAM,GACtB,cAAe,CAAC,CAAE,QAAAC,CAAQ,IAAMA,EAAQ,uBAAyB,QAAU,CAACA,EAAQ,YACtF,EACA,QAAS,CACP,UAAW,IAAM,CAAC,EAClB,gBAAiB,IAAM,CAAC,EACxB,yBAA0B,IAAM,CAAC,EACjC,kBAAmB,IAAM,CAAC,EAC1B,yBAA0B,IAAM,CAAC,EACjC,kBAAmB,IAAM,CAAC,EAC1B,WAAY,IAAM,CAAC,EACnB,iBAAkB,IAAM,CAAC,EACzB,eAAgB,IAAM,CAAC,EACvB,iBAAkB,IAAM,CAAC,EACzB,eAAgB,IAAM,CAAC,EACvB,UAAW,IAAM,CAAC,EAClB,aAAc,IAAM,CAAC,EACrB,kBAAmB,IAAM,CAAC,EAC1B,YAAa,IAAM,CAAC,EACpB,iBAAkB,IAAM,CAAC,EACzB,2BAA4B,IAAM,CAAC,EACnC,eAAgBC,EAAO,CAAE,YAAa,IAAM,EAAK,CAAC,EAClD,iBAAkBA,EAAO,CAAE,YAAa,IAAM,EAAM,CAAC,EACrD,aAAcA,EAAO,CAAE,UAAW,IAAM,EAAK,CAAC,EAC9C,eAAgBA,EAAO,CAAE,UAAW,IAAM,EAAM,CAAC,EACjD,gBAAiBA,EAAO,CAAE,qBAAsB,IAAM,SAAkC,CAAC,EACzF,eAAgBA,EAAO,CAAE,qBAAsB,IAAM,QAAiC,CAAC,EACvF,cAAeA,EAAO,CAAE,qBAAsB,IAAM,OAAgC,CAAC,EACrF,0BAA2BA,EAAO,CAAE,0BAA2B,IAAM,KAAM,kBAAmB,IAAM,EAAM,CAAC,EAC3G,mBAAoBA,EAAO,CACzB,UAAW,IAAM,GACjB,qBAAsB,IAAM,SAC5B,aAAc,IAAM,UACtB,CAAC,EACD,oBAAqBA,EAAO,CAC1B,UAAW,IAAM,GACjB,qBAAsB,IAAM,SAC5B,aAAc,IAAM,WACpB,0BAA2B,CAAC,CAAE,MAAAC,CAAM,IACjCA,EAAqD,IAC1D,CAAC,EACD,wBAAyBD,EAAO,CAC9B,UAAW,IAAM,GACjB,0BAA2B,IAAM,KACjC,kBAAmB,IAAM,EAC3B,CAAC,EACD,qBAAsBA,EAAO,CAAE,kBAAmB,IAAM,EAAK,CAAC,EAC9D,eAAgBA,EAAO,CACrB,YAAa,CAAC,CAAE,MAAAC,CAAM,IAAOA,EAAgD,GAC/E,CAAC,EACD,oBAAqBD,EAAO,CAAE,0BAA2B,IAAM,IAAK,CAAC,EACrE,uBAAwBA,EAAO,CAC7B,UAAW,IAAM,GACjB,aAAc,IAAM,UACtB,CAAC,EACD,wBAAyBA,EAAO,CAC9B,aAAc,IAAM,UACtB,CAAC,EACD,uBAAwBA,EAAO,CAC7B,kBAAmB,IAAM,EAC3B,CAAC,CACH,CACF,CAAC,EAAE,cAAc,CACf,GAAI,QACJ,QAAS,OACT,QAASJ,EAET,GAAI,CACF,YAAa,CACX,MAAO,gBACP,QAAS,CACPI,EAAO,CACL,qBAAsB,IAAM,UAC5B,aAAc,IAAM,EACtB,CAAC,EACDE,EAAW,cAAe,CACxB,GAAI,cACJ,MAAO,CAAC,CAAE,QAAAH,CAAQ,KAAO,CACvB,SAAUA,EAAQ,SAClB,YAAaA,EAAQ,YACrB,SAAUA,EAAQ,QACpB,EACF,CAAC,CACH,CACF,CACF,EAEA,OAAQ,CAIN,KAAM,CACJ,GAAI,CACF,YAAa,CACX,QAAS,CAAC,kBAAkB,CAC9B,EACA,WAAY,CACV,QAAS,CACP,aACA,oBACA,cACA,mBACA,2BACF,CACF,EACA,SAAU,CACR,QAAS,CACP,cACA,oBACA,2BACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,CACE,MAAO,iBACP,QAAS,CAAC,iBAAkB,4BAA4B,CAC1D,EACA,CACE,OAAQ,QAOR,QAAS,CAAC,eAAgB,YAAa,oBAAqB,4BAA4B,CAC1F,CACF,EACA,cAAe,CACb,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,iBAAkB,CAChB,OAAQ,WACR,QAAS,CACP,sBACA,0BACF,CACF,EACA,QAAS,CAAE,OAAQ,SAAU,EAC7B,eAAgB,CACd,QAAS,iBACX,EACA,aAAc,CACZ,CACE,MAAO,CAAC,CAAE,QAAAA,CAAQ,IAAiCA,EAAQ,YAC3D,OAAQ,WACR,QAAS,CAAC,mBAAoB,qBAAsB,kBAAmB,oBAAqB,mBAAmB,CACjH,EACA,CACE,QAAS,CAAC,iBAAkB,mBAAmB,CACjD,CACF,EACA,aAAc,CACZ,QAAS,CAAC,gBAAiB,kBAAkB,CAC/C,EACA,aAAc,CACZ,QAAS,gBACX,CACF,CACF,EAKA,MAAO,CACL,GAAI,CACF,MAAO,CACL,QAAS,CAAC,iBAAkB,aAAc,mBAAoB,gBAAgB,CAChF,EACA,KAAM,CACJ,QAAS,CAAC,eAAgB,YAAa,mBAAmB,CAC5D,EACA,eAAgB,CACd,QAAS,iBACX,EACA,cAAe,CACb,OAAQ,WACR,QAAS,oBACX,EAWA,aAAc,CACZ,OAAQ,WACR,QAAS,CACP,iBACA,2BACA,0BACA,yBACA,mBACF,CACF,EACA,aAAc,CACZ,QAAS,eACX,EACA,KAAM,CACJ,QAAS,CACP,YACA,gBACF,CACF,EACA,kBAAmB,CACjB,QAAS,sBACX,EACA,YAAa,CACX,OAAQ,OACR,QAAS,CAAC,iBAAkB,mBAAoB,kBAAkB,CACpE,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,aACA,mBACA,cACA,mBACF,CACF,EACA,aAAc,CACZ,QAAS,gBACX,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAc,oBAAqB,cAAe,kBAAkB,CAClG,CACF,CACF,EAKA,QAAS,CACP,GAAI,CACF,eAAgB,CACd,QAAS,iBACX,EACA,aAAc,CACZ,CACE,MAAO,CAAC,CAAE,QAAAA,CAAQ,IAAiCA,EAAQ,YAC3D,OAAQ,WACR,QAAS,CAAC,mBAAoB,qBAAsB,kBAAmB,oBAAqB,mBAAmB,CACjH,EACA,CACE,OAAQ,OACR,QAAS,CAAC,iBAAkB,mBAAmB,CACjD,CACF,EACA,aAAc,CACZ,OAAQ,OACR,QAAS,CAAC,gBAAiB,kBAAkB,CAC/C,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,CACE,MAAO,iBACP,QAAS,CAAC,iBAAkB,4BAA4B,CAC1D,EACA,CACE,OAAQ,QAOR,QAAS,CAAC,eAAgB,YAAa,oBAAqB,4BAA4B,CAC1F,CACF,EACA,cAAe,CACb,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,iBAAkB,CAChB,OAAQ,WACR,QAAS,CACP,sBACA,0BACF,CACF,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,cACA,mBACF,CACF,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAa,CAC3C,EACA,aAAc,CACZ,QAAS,gBACX,CACF,CACF,EAKA,SAAU,CACR,GAAI,CACF,MAAO,CACL,QAAS,CACP,iBACA,mBACA,iBACA,mBACA,gBACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,QAAS,CAAC,eAAgB,kBAAmB,mBAAmB,CAClE,EACA,CACE,QAAS,cACX,CACF,EACA,cAAe,CACb,QAAS,wBACX,EACA,KAAM,CACJ,QAAS,CACP,sBACA,eACA,gBACF,CACF,EACA,WAAY,CAAC,EACb,eAAgB,CACd,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,mBACA,aACF,CACF,EACA,kBAAmB,CACjB,QAAS,sBACX,EACA,eAAgB,CACd,OAAQ,OACR,QAAS,CAAC,iBAAkB,mBAAoB,kBAAkB,CACpE,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,mBACA,cACA,mBACF,CACF,EAEA,WAAY,CACV,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,cACA,oBACA,kBACF,CACF,CACF,CACF,CACF,CACF,CAAC,CACH,CDjdA,IAAMI,EAA6B,EAG7BC,EAAgB,GAUhBC,EAAqB,IAGdC,EAAN,KAAY,CACR,MACA,SAED,UACA,aACS,SAET,oBAA0C,KAGzC,MAGQ,gBAEjB,IAAY,KAA2B,CACrC,GAAI,KAAK,kBAAoB,aAAc,OAAO,KAClD,IAAMC,EAAUC,EAAgB,EAChC,GAAID,GAAW,CAAC,KAAK,SAAU,CAC7B,KAAK,SAAWA,EAAQ,WAAW,EACnC,KAAK,SAAS,KAAK,MAAQ,KAAK,MAAM,OACtC,KAAK,SAAS,QAAQA,EAAQ,WAAW,EAiBzC,GAAI,CACF,KAAK,oBAAsBA,EAAQ,yBAAyB,KAAK,KAAK,EACtE,KAAK,eAAiBA,EAAQ,WAAW,EACzC,KAAK,eAAe,KAAK,MAAQ,EACjC,KAAK,oBAAoB,QAAQ,KAAK,cAAc,EACpD,KAAK,eAAe,QAAQ,KAAK,QAAQ,EACzC,KAAK,MAAM,OAAS,CACtB,MAAQ,CACN,KAAK,oBAAsB,KAC3B,KAAK,eAAiB,IACxB,CACF,CACA,OAAOA,CACT,CAEQ,SAA4B,KAC5B,oBAA0D,KAC1D,eAAkC,KAClC,WAA2C,KACnD,YAAkC,KAkB1B,uBAAyB,EACzB,yBAA2B,GAE3B,cAAgB,EAEhB,gBAAkB,EAElB,kBAAoB,EAEX,OAGA,SACT,MAAuB,KACvB,0BAA4B,GAEpC,YAAYE,EAMT,CACD,KAAK,MAAQA,EAAK,MAClB,KAAK,UAAYA,EAAK,SACtB,KAAK,aAAeA,EAAK,SACzB,KAAK,SAAWA,EAAK,UAAY,GACjC,KAAK,SAAWA,EAAK,UAAY,CAAC,EAClC,KAAK,SAAWA,EAAK,MAGrB,KAAK,MAAQ,IAAI,MACjB,KAAK,MAAM,QAAU,OASjBA,EAAK,MAAM,iBAAmB,eAChC,KAAK,MAAM,YAAc,aAE3B,KAAK,MAAM,IAAM,KAAK,UACtB,KAAK,MAAM,OAASA,EAAK,MAAM,OAC/B,KAAK,MAAM,SAAW,GACtB,KAAK,MAAM,QAAU,IAAM,CArK/B,IAAAC,EAAAC,EAsKM,IAAMC,GAAOF,EAAA,KAAK,MAAM,QAAX,YAAAA,EAAkB,KAC/B,GAAIE,IAAS,EAAG,OAChB,IAAMC,IAAMF,EAAA,KAAK,MAAM,QAAX,YAAAA,EAAkB,UAAW,UACzC,KAAK,SAAS,QACZ,IAAI,MAAM,8BAA8B,KAAK,KAAK,UAAUC,CAAI,MAAMC,CAAG,EAAE,CAC7E,CACF,EACA,KAAK,MAAM,QAAU,IAAM,CACzB,KAAK,SAAS,QACZ,uBAAuB,KAAK,KAAK,iBAAiB,KAAK,OAAO,YAAY,EAAE,KAAK,aAAa,KAAK,SAAS,iBAAiB,EAC/H,EACA,KAAK,OAAO,KAAK,CAAE,KAAM,aAAc,CAAC,CAC1C,EAEA,KAAK,gBAAkBJ,EAAK,MAAM,eAElC,IAAMK,EAA+B,CACnC,SAAU,KAAK,UACf,YAAa,KAAK,UAClB,SAAU,KAAK,SACf,aAAc,QACd,qBAAsB,OACtB,UAAW,GACX,0BAA2B,KAC3B,kBAAmB,GACnB,aAAc,GACd,YAAa,EACf,EACMC,EAAUC,EAAmBF,CAAc,EAAE,QAAQ,CACzD,OAAQ,CACN,gBAAiB,IAAM,CAAC,EAAE,KAAK,KAAO,KAAK,aAAe,KAAK,UAC/D,eAAgB,IAAM,KAAK,kBAAoB,eACjD,EACA,OAAQ,CACN,YAAaG,EAAmB,QAAQ,CACtC,OAAQ,CACN,WAAYC,EAAY,MAAO,CAAE,OAAAC,CAAO,IAAM,CAC5C,IAAMC,EAAM,MAAM,MAAM,KAAK,UAAW,CAAE,OAAQ,OAAQ,OAAAD,CAAO,CAAC,EAClE,OAAIC,EAAI,YAAcA,EAAI,KAYxB,KAAK,aAAeA,EAAI,IACjBA,EAAI,KAEN,IACT,CAAC,EACD,WAAYF,EAAY,MAAO,CAAE,MAAAG,EAAO,OAAAF,CAAO,IAAM,CACnD,GAAM,CAAE,YAAAG,CAAY,EAAID,EAClBD,EAAM,MAAM,MAAME,EAAa,CAAE,OAAAH,CAAO,CAAC,EAC/C,GAAI,CAACC,EAAI,GAAI,MAAM,IAAI,MAAM,QAAQA,EAAI,MAAM,QAAQE,CAAW,EAAE,EACpE,KAAK,oBAAsB,MAAMF,EAAI,YAAY,CACnD,CAAC,EACD,YAAaF,EAAY,SAAY,CACnC,IAAMK,EAAM,KAAK,oBAEjB,GADA,KAAK,oBAAsB,KACvB,CAACA,GAAO,CAAC,KAAK,IAAK,MAAM,IAAI,MAAM,gCAAgC,EACvE,KAAK,YAAc,MAAM,KAAK,IAAI,gBAAgBA,CAAG,EACrD,KAAK,6BAA6B,CASpC,CAAC,CACH,CACF,CAAC,CACH,EACA,QAAS,CACP,2BAA4B,IAAM,CAChC,KAAK,QAAQ,EACbC,EAAmB,CACrB,EACA,UAAW,IAAM,KAAK,WAAW,EACjC,gBAAiB,IAAM,CACrB,KAAK,iBAAiB,KAAK,iBAAiB,CAC9C,EACA,yBAA0B,CAAC,CAAE,QAAAjB,CAAQ,IAAiC,CACpE,KAAK,0BAA0BA,EAAQ,SAAS,CAClD,EACA,yBAA0B,CAAC,CAAE,QAAAA,CAAQ,IAAiC,CACpE,IAAMkB,EAAOlB,EAAQ,0BACjBkB,IAAS,MAAQ,CAAC,KAAK,KAAO,CAAC,KAAK,aAAe,CAAC,KAAK,WAC7D,KAAK,6BAA6B,EAClC,KAAK,gBAAgB,EACrB,KAAK,WAAa,KAAK,IAAI,mBAAmB,EAC9C,KAAK,WAAW,OAAS,KAAK,YAC9B,KAAK,WAAW,aAAa,MAAQ,KAAK,SAAS,aACnD,KAAK,WAAW,QAAQ,KAAK,QAAQ,EAIrC,KAAK,WAAW,QAAU,KAAK,qBAK/B,KAAK,WAAW,MAAMA,EAAM,KAAK,sBAAsB,EACvD,KAAK,cAAgBA,EACrB,KAAK,gBAAkB,EACvB,KAAK,SAAS,QACZ,kCAAkC,KAAK,KAAK,SAASA,EAAK,QAAQ,CAAC,CAAC,WAAW,KAAK,IAAI,YAAY,QAAQ,CAAC,CAAC,WAAWA,EAAO,KAAK,IAAI,aAAa,QAAQ,CAAC,CAAC,aAAa,KAAK,uBAAuB,QAAQ,CAAC,CAAC,GACrN,EACF,EACA,kBAAmB,IAAM,KAAK,kBAAkB,EAChD,WAAY,IAAM,KAAK,MAAM,MAAM,EACnC,iBAAkB,IAAM,CACtB,IAAMC,EAAI,KAAK,YACf,KAAK,kBAAoB,SAASA,CAAC,EAAIA,EAAI,CAC7C,EACA,eAAgB,IAAM,KAAK,gBAAgB,EAC3C,iBAAkB,IAAM,KAAK,kBAAkB,EAC/C,eAAgB,IAAM,CACpB,eAAe,IAAM,KAAK,SAAS,WAAW,KAAK,OAAO,CAAC,CAAC,CAC9D,EACA,UAAW,IAAM,KAAK,WAAW,EACjC,aAAc,IAAM,KAAK,cAAc,EACvC,kBAAmB,IAAM,CACvB,KAAK,MAAM,YAAc,CAC3B,EACA,YAAa,IAAM,CACjB,KAAK,cAAgB,EACrB,KAAK,gBAAkB,EACvB,KAAK,kBAAoB,CAC3B,EACA,iBAAkB,IAAM,CACtB,eAAe,IAAM,KAAK,SAAS,aAAa,IAAI,CAAC,CACvD,EACA,kBAAmB,IAAM,CAIvB,eAAe,IAAM,KAAK,SAAS,mBAAmB,IAAI,CAAC,CAC7D,CACF,CACF,CAAC,EACD,KAAK,OAASC,EAAYZ,CAAO,EACjC,KAAK,OAAO,MAAM,CACpB,CAMA,MAAa,CACX,KAAK,SAAS,QACZ,sBAAsB,KAAK,KAAK,iBAAiB,KAAK,OAAO,YAAY,EAAE,KAAK,cAAc,CAAC,CAAC,KAAK,WAAW,WAAW,CAAC,CAAC,KAAK,GAAG,gBAAgB,KAAK,MAAM,MAAM,EACxK,EACA,KAAK,OAAO,KAAK,CAAE,KAAM,MAAO,CAAC,CACnC,CAEA,OAAc,CACZ,KAAK,OAAO,KAAK,CAAE,KAAM,OAAQ,CAAC,CACpC,CAEA,KAAKa,EAAoB,CACvB,GAAI,CAAC,SAASA,CAAI,EAAG,OACrB,IAAMC,EAAU,KAAK,IAAI,EAAG,MAAM,KAAK,QAAQ,EAAID,EAAO,KAAK,IAAIA,EAAM,KAAK,QAAQ,CAAC,EACvF,KAAK,kBAAoBC,EACzB,KAAK,OAAO,KAAK,CAAE,KAAM,OAAQ,KAAMA,CAAQ,CAAC,CAClD,CAEA,UAAUC,EAAiB,CACzB,IAAMC,EAAM,KAAK,IAAI,EAAG,KAAK,IAAI,EAAGD,CAAC,CAAC,EAKlC,KAAK,oBACP,KAAK,MAAM,OAAS,EAEpB,KAAK,MAAM,OAASC,EAElB,KAAK,WAAU,KAAK,SAAS,KAAK,MAAQA,GAC9C,KAAK,OAAO,KAAK,CAAE,KAAM,aAAc,OAAQA,CAAI,CAAC,CACtD,CAEA,gBAAgBC,EAAoB,CAElC,GAAI,KAAK,KAAO,KAAK,YAAc,KAAK,OAAO,YAAY,EAAE,QAAQ,UAAW,CAC9E,IAAMC,EAAU,KAAK,WAAW,aAAa,MAC7C,KAAK,gBAAkB,KAAK,iBAAmB,KAAK,IAAI,YAAc,KAAK,eAAiBA,EAC5F,KAAK,cAAgB,KAAK,IAAI,WAChC,CACA,KAAK,MAAM,aAAeD,EACtB,KAAK,aAAY,KAAK,WAAW,aAAa,MAAQA,EAC5D,CAEA,SAAgB,CACd,KAAK,SAAS,QACZ,mBAAmB,KAAK,KAAK,UAAU,KAAK,OAAO,YAAY,EAAE,KAAK,cAAc,CAAC,CAAC,KAAK,WAAW,WAAW,CAAC,CAAC,KAAK,GAAG,EAC7H,EACI,KAAK,OAAO,YAAY,EAAE,QAAU,QACtC,KAAK,OAAO,KAAK,CAAE,KAAM,SAAU,CAAC,EAElC,MAAK,cACTR,EAAmB,EACd,KAAK,KACV,KAAK,OAAO,KAAK,CAAE,KAAM,aAAc,CAAC,EAC1C,CAEA,UAAUU,EAAiB,EAAS,CAClC,IAAMC,EAAM,KAAK,SACb,CAAC,MAAMA,CAAG,GAAKA,EAAMD,GACvB,KAAK,KAAKC,EAAMD,CAAc,CAElC,CAEA,UAAiB,CACf,KAAK,OAAO,KAAK,CAAE,KAAM,UAAW,CAAC,CACvC,CAEA,YAAmB,CACjB,KAAK,SAAS,QACZ,4BAA4B,KAAK,KAAK,iBAAiB,KAAK,OAAO,YAAY,EAAE,KAAK,cAAc,KAAK,SAAS,EACpH,EACA,KAAK,0BAA4B,GACjC,KAAK,OAAO,KAAK,CAAE,KAAM,YAAa,CAAC,EACvC,KAAK,SAAS,QACZ,iCAAiC,KAAK,KAAK,iBAAiB,KAAK,OAAO,YAAY,EAAE,KAAK,EAC7F,CACF,CAEA,SAAgB,CAhZlB,IAAAxB,EAiZI,KAAK,WAAW,EAChB,KAAK,oBAAsB,KAC3B,KAAK,YAAc,MACnBA,EAAA,KAAK,WAAL,MAAAA,EAAe,aACf,KAAK,SAAW,KAChB,KAAK,OAAO,KAAK,CACnB,CAMA,oBAA2B,CACZ,KAAK,OAAO,YAAY,EAC5B,QAAQ,4BAA8B,MAC/C,KAAK,OAAO,KAAK,CAAE,KAAM,gBAAiB,CAAC,CAC7C,CAEA,qBAAqBe,EAAoB,CACnC,CAAC,KAAK,KAAO,CAAC,KAAK,aAAe,CAAC,KAAK,UAC5C,KAAK,OAAO,KAAK,CAAE,KAAM,mBAAoB,KAAAA,CAAK,CAAC,CACrD,CAMA,IAAI,aAAsB,CACxB,IAAMW,EAAO,KAAK,OAAO,YAAY,EACrC,OAAIA,EAAK,QAAU,WACZA,EAAK,QAAQ,UACb,KAAK,IACH,KAAK,IAAI,EAAG,KAAK,iBAAmB,KAAK,IAAI,YAAc,KAAK,eAAiB,KAAK,SAAS,YAAY,EAD5F,EADc,KAAK,kBAIpC,KAAK,MAAM,WACpB,CAEA,IAAI,UAAmB,CAMrB,OALa,KAAK,OAAO,YAAY,EAK5B,QAAU,SAAW,CAAC,MAAM,KAAK,MAAM,QAAQ,EAC/C,KAAK,MAAM,SAEhB,KAAK,YAAoB,KAAK,YAAY,SACvC,KAAK,MAAM,QACpB,CAEA,IAAI,UAAoB,CACtB,IAAMA,EAAO,KAAK,OAAO,YAAY,EACrC,OAAIA,EAAK,QAAU,WAAmB,CAACA,EAAK,QAAQ,UAC7C,KAAK,MAAM,MACpB,CAEA,IAAI,WAAqB,CACvB,OAAO,KAAK,OAAO,YAAY,EAAE,QAAQ,SAC3C,CAEA,IAAI,UAAmB,CACrB,OAAO,KAAK,YACd,CAEA,IAAI,cAA6B,CAC/B,OAAO,KAAK,OAAO,YAAY,EAAE,QAAQ,YAC3C,CAEA,IAAI,sBAA6C,CAC/C,OAAO,KAAK,OAAO,YAAY,EAAE,QAAQ,oBAC3C,CAEA,IAAI,eAAyB,CAC3B,OAAO,KAAK,aAAe,IAC7B,CAEA,IAAI,cAAuB,CACzB,OAAO,KAAK,OAAO,YAAY,EAAE,KACnC,CAEA,IAAI,2BAA2C,CAC7C,OAAO,KAAK,OAAO,YAAY,EAAE,QAAQ,yBAC3C,CAEA,IAAI,gBAA0B,CAC5B,OAAO,KAAK,cAAgB,IAC9B,CAEA,QAAoB,CAzetB,IAAA1B,EA0eI,MAAO,CACL,MAAO,KAAK,MACZ,YAAa,KAAK,YAClB,SAAU,KAAK,SACf,UAAW,KAAK,UAChB,SAAU,KAAK,SACf,SAAQA,EAAA,KAAK,WAAL,YAAAA,EAAe,KAAK,QAAS,KAAK,MAAM,OAChD,SAAU,KAAK,SACf,aAAc,KAAK,aACnB,qBAAsB,KAAK,qBAC3B,SAAU,KAAK,SACf,aAAc,KAAK,SAAS,aAC5B,aAAc,KAAK,YACrB,CACF,CAMQ,YAAmB,CACrB,KAAK,MAAM,UAAY,SAAQ,KAAK,MAAM,QAAU,QACxD,KAAK,MAAM,aAAe,KAAK,SAAS,aACxC,IAAM2B,EAAU,KAAK,MAAM,KAAK,EAC5BA,GACFA,EAAQ,MAAOC,GAAiB,CAC1BA,aAAe,OAASA,EAAI,OAAS,kBACvC,KAAK,SAAS,cAAc,EACnBA,aAAe,OAASA,EAAI,OAAS,cAG9C,KAAK,SAAS,QAAQA,aAAe,MAAQA,EAAM,IAAI,MAAM,OAAOA,CAAG,CAAC,CAAC,CAE7E,CAAC,CAEL,CAEQ,YAAmB,CACzB,IAAMT,EAAU,KAAK,kBAChB,SAASA,CAAO,IACjB,KAAK,MAAM,UAAY,SAAQ,KAAK,MAAM,QAAU,QACpD,KAAK,MAAM,YAAc,iBAAiB,cAC5C,KAAK,MAAM,YAAcA,GAEzB,KAAK,MAAM,iBACT,iBACA,IAAM,CACJ,KAAK,MAAM,YAAcA,CAC3B,EACA,CAAE,KAAM,EAAK,CACf,EACA,KAAK,MAAM,KAAK,GAEpB,CA2BQ,0BAA0BU,EAA2B,CAC3D,GAAI,CAAC,KAAK,KAAO,CAAC,KAAK,aAAe,CAAC,KAAK,SAAU,OAEtD,IAAMC,EAAS,KAAK,MAAM,YAG1B,GAFA,KAAK,kBAAoB,SAASA,CAAM,EAAIA,EAAS,EAEjD,CAACD,EAAY,CACf,KAAK,MAAM,MAAM,EACjB,KAAK,SAAS,QACZ,kCAAkC,KAAK,KAAK,WAAW,KAAK,kBAAkB,QAAQ,CAAC,CAAC,mBAC1F,EACA,MACF,CAeA,IAAME,EAAK,KAAK,IAAI,YACdC,EAAKD,EAAKpC,EAEhB,GAAI,KAAK,eAAgB,CACvB,KAAK,eAAe,KAAK,sBAAsBoC,CAAE,EACjD,KAAK,eAAe,KAAK,eAAe,EAAGA,CAAE,EAC7C,KAAK,eAAe,KAAK,wBAAwB,EAAGC,CAAE,EAKtD,IAAMC,EAAS,KAAK,IACdC,EAAe,KAAK,eAC1B,WAAW,IAAM,CACf,KAAK,MAAM,MAAM,EACbD,GAAUC,IACZA,EAAa,KAAK,sBAAsBD,EAAO,WAAW,EAC1DC,EAAa,KAAK,eAAe,EAAGD,EAAO,WAAW,EAE1D,EAAGtC,EAAqB,IAAO,CAAC,CAClC,KAAO,CAEL,IAAMwC,EAAc,KAAK,MAAM,OAC/B,KAAK,MAAM,OAAS,EACpB,KAAK,MAAM,MAAM,EACjB,KAAK,MAAM,OAASA,CACtB,CAEA,KAAK,iBAAiB,KAAK,kBAAmBxC,CAAkB,EAEhE,KAAK,SAAS,QACZ,kCAAkC,KAAK,KAAK,WAAW,KAAK,kBAAkB,QAAQ,CAAC,CAAC,yBAAyBA,CAAkB,iBAAiB,CAAC,CAAC,KAAK,cAAc,EAC3K,CACF,CAUQ,8BAAqC,CACtC,KAAK,cACN,KAAK,2BACT,KAAK,uBAAyB,EAC9B,KAAK,yBAA2B,GAChC,KAAK,SAAS,QACZ,sCAAsC,KAAK,KAAK,cAAc,KAAK,YAAY,SAAS,QAAQ,CAAC,CAAC,cAAc,MAAM,KAAK,MAAM,QAAQ,EAAI,MAAQ,KAAK,MAAM,SAAS,QAAQ,CAAC,EAAI,GAAG,gDAC3L,GACF,CAEQ,iBAAiBmC,EAAgBM,EAAY,EAAS,CAC5D,GAAI,GAAC,KAAK,KAAO,CAAC,KAAK,aAAe,CAAC,KAAK,UAqB5C,IAlBA,KAAK,6BAA6B,EAClC,KAAK,gBAAgB,EAIjB,KAAK,IAAI,QAAU,aACrB,KAAK,IAAI,OAAO,EAGlB,KAAK,WAAa,KAAK,IAAI,mBAAmB,EAC9C,KAAK,WAAW,OAAS,KAAK,YAC9B,KAAK,WAAW,aAAa,MAAQ,KAAK,SAAS,aAO/CA,EAAY,EAAG,CACjB,IAAMC,EAAW,KAAK,IAAI,WAAW,EAC/BN,EAAK,KAAK,IAAI,YACpBM,EAAS,KAAK,eAAe,EAAGN,CAAE,EAClCM,EAAS,KAAK,wBAAwB,EAAGN,EAAKK,CAAS,EACvD,KAAK,WAAW,QAAQC,CAAQ,EAChCA,EAAS,QAAQ,KAAK,QAAQ,CAChC,MACE,KAAK,WAAW,QAAQ,KAAK,QAAQ,EAGvC,KAAK,WAAW,QAAU,KAAK,qBAE/B,KAAK,cAAgB,KAAK,IAAI,YAC9B,KAAK,gBAAkBP,EAIvB,KAAK,WAAW,MAAM,EAAGA,EAAS,KAAK,sBAAsB,EAC/D,CAEQ,iBAAwB,CAC9B,GAAK,KAAK,WACV,MAAK,WAAW,QAAU,KAC1B,GAAI,CACF,KAAK,WAAW,KAAK,CACvB,MAAQ,CAER,CACA,GAAI,CACF,KAAK,WAAW,WAAW,CAC7B,MAAQ,CAER,CACA,KAAK,WAAa,KACpB,CAEQ,eAAsB,CAE5B,IAAMD,EADO,KAAK,OAAO,YAAY,EACb,QAAQ,UAC1BV,EAAU,KAAK,kBACrB,KAAK,gBAAgB,EACjBU,GACF,KAAK,iBAAiBV,CAAO,CAEjC,CAEQ,qBAAuB,IAAY,CACzC,KAAK,SAAS,QACZ,8BAA8B,KAAK,KAAK,eAAe,CAAC,CAAC,KAAK,UAAU,aAAa,KAAK,SAAS,iBAAiB,EACtH,EACK,KAAK,YACV,KAAK,OAAO,KAAK,CAAE,KAAM,gBAAiB,CAAC,CAC7C,EAMA,mBAA0B,CACxB,GAAI,KAAK,QAAU,KAAM,OACzB,IAAMmB,EAAO,IAAM,CACjB,GAAI,KAAK,UAAY,CAAC,KAAK,UAAW,CACpC,KAAK,MAAQ,KACb,MACF,CACA,KAAK,SAAS,WAAW,KAAK,OAAO,CAAC,EAEtC,IAAMC,EAAY,KAAK,SAAW,KAAK,YAGrC,CAFW,KAAK,OAAO,YAAY,EAE7B,QAAQ,mBACd,CAAC,MAAMA,CAAS,GAChBA,GAAa9C,IAEb,KAAK,OAAO,KAAK,CAAE,KAAM,mBAAoB,CAAC,EAC9C,eAAe,IAAM,KAAK,SAAS,mBAAmB,IAAI,CAAC,GAG7D,IAAM+C,EAAmB,MAAM,KAAK,QAAQ,EAAI9C,EAAgB,KAAK,IAAI,KAAK,SAAW,GAAKA,CAAa,EACvG,CAAC,KAAK,2BAA6B,KAAK,aAAe8C,IACzD,KAAK,0BAA4B,GACjC,eAAe,IAAM,KAAK,SAAS,eAAe,IAAI,CAAC,GAGzD,KAAK,MAAQ,sBAAsBF,CAAI,CACzC,EACA,KAAK,MAAQ,sBAAsBA,CAAI,CACzC,CAEQ,mBAA0B,CAC5B,KAAK,QAAU,OACjB,qBAAqB,KAAK,KAAK,EAC/B,KAAK,MAAQ,KAEjB,CACF,EG9vBO,SAASG,EACdC,EACAC,EACkC,CAClC,IAAIC,EAAO,EACX,MAAO,IAAIC,IAAwB,CACjC,IAAMC,EAAM,YAAY,IAAI,EACxBA,EAAMF,EAAOD,IACjBC,EAAOE,EACPJ,EAAG,GAAGG,CAAI,EACZ,CACF,CPKA,IAAME,EAAyB,EAElBC,EAAN,KAAqC,CAClC,QAAmB,CAAC,EACX,OAEA,YACA,SACA,iBACA,qBACA,iBACA,SACA,eACA,SAER,eAED,QACA,kBACA,cAGA,oBAAqC,KAErC,8BAAgCC,EACtC,CAACC,EAAkBC,EAAqBC,IACtCC,EAAgCH,EAAUC,EAAaC,CAAY,EACrE,GACF,EAEA,YAAYE,EAA0B,CAAC,EAAG,CACxC,GAAM,CACJ,OAAAC,EAAS,CAAC,EACV,WAAAC,EACA,QAAAC,EACA,gBAAAC,EACA,oBAAAC,EACA,gBAAAC,EACA,QAAAC,EACA,cAAAC,EACA,QAAAC,EACA,eAAAC,EAAiB,SACjB,cAAAC,EAAgB,CAAC,EACjB,OAAQC,EAAgB,EACxB,iBAAAC,EAAmB,EACnB,aAAcC,EAAsB,CACtC,EAAId,EAEJ,KAAK,QAAU,KAAK,IAAI,EAAG,KAAK,IAAI,EAAGY,CAAa,CAAC,EACrD,KAAK,kBAAoB,KAAK,IAAI,EAAGC,CAAgB,EACrD,KAAK,cAAgB,KAAK,IAAI,EAAG,KAAK,IAAI,IAAMC,CAAmB,CAAC,EACpE,KAAK,eAAiBJ,EACtB,KAAK,YAAcR,EACnB,KAAK,SAAWC,EAChB,KAAK,iBAAmBC,EACxB,KAAK,qBAAuBC,EAC5B,KAAK,iBAAmBC,EACxB,KAAK,SAAWC,EAChB,KAAK,eAAiBC,EACtB,KAAK,SAAWC,EAEhB,KAAK,QAAUR,EAAO,IACpB,CAACc,EAAK,IACJ,IAAIC,EAAM,CACR,SAAUD,EACV,MAAO,EACP,MAAO,KACP,SAAUJ,EAAc,CAAC,CAC3B,CAAC,CACL,EAUA,IAAMM,EAAUC,EAAmB,CACjC,kBAAmB,EACnB,WAAY,KAAK,QAAQ,MAC3B,CAAC,EAAE,QAAQ,CACT,QAAS,CACP,kBAAmB,CAAC,CAAE,QAAAC,CAAQ,IAAM,CAtG5C,IAAAC,GAuGUA,EAAA,KAAK,SAASD,EAAQ,iBAAiB,IAAvC,MAAAC,EAA0C,YAC5C,EACA,qBAAsB,CAAC,CAAE,QAAAD,CAAQ,IAAM,CAzG/C,IAAAC,GA0GUA,EAAA,KAAK,SAASD,EAAQ,iBAAiB,IAAvC,MAAAC,EAA0C,YAC5C,EACA,uBAAwB,CAAC,CAAE,QAAAD,CAAQ,IAAM,CACvC,IAAME,EAAQ,KAAK,SAASF,EAAQ,iBAAiB,EAChDE,IACLA,EAAM,SAAS,EACX,KAAK,sBAAwBA,EAAM,OACrCA,EAAM,KAAK,EAEf,EACA,sBAAuB,CAAC,CAAE,QAAAF,CAAQ,IAAM,CACtC,IAAMG,EAAM,KAAK,SAASH,EAAQ,iBAAiB,EAC9CG,IACD,KAAK,sBAAwBA,EAAI,MACnCA,EAAI,KAAK,GAET,KAAK,oBAAsB,KAC3B,KAAK,QACH,+BAA+BA,EAAI,KAAK,sBAAiBA,EAAI,aAAa,cAAcA,EAAI,SAAS,iBAAiBA,EAAI,YAAY,EACxI,EACAA,EAAI,kBAAkB,GAE1B,EACA,iBAAkB,IAAM,KAAK,wBAAwB,EACrD,oBAAqB,CAAC,CAAE,QAAAH,CAAQ,IAAM,CAlI9C,IAAAC,EAmIU,IAAME,EAAM,KAAK,SAASH,EAAQ,iBAAiB,EAC/CG,KAAKF,EAAA,KAAK,mBAAL,MAAAA,EAAA,UAAwBE,EAAI,OAAO,GAC9C,EACA,oBAAqB,CAAC,CAAE,QAAAH,CAAQ,IAAM,CAtI9C,IAAAC,EAuIU,IAAME,EAAM,KAAK,SAASH,EAAQ,iBAAiB,EAC/CG,KAAKF,EAAA,KAAK,mBAAL,MAAAA,EAAA,UAAwBE,EAAI,OAAO,GAC9C,EACA,wBAAyB,CAAC,CAAE,QAAAH,CAAQ,IAAM,CA1IlD,IAAAC,EA2IU,IAAME,EAAM,KAAK,SAASH,EAAQ,iBAAiB,EAC/CG,KAAKF,EAAA,KAAK,uBAAL,MAAAA,EAAA,UAA4BE,EAAI,OAAO,GAClD,EACA,YAAa,IAAG,CA9IxB,IAAAF,EA8I2B,OAAAA,EAAA,KAAK,WAAL,YAAAA,EAAA,YACnB,2BAA4B,CAAC,CAAE,QAAAD,CAAQ,IAAM,CAC3C,IAAMG,EAAM,KAAK,SAASH,EAAQ,iBAAiB,EAC/CG,GAAKC,EAA2BD,EAAI,QAAQ,CAClD,EACA,aAAc,CAAC,CAAE,QAAAH,CAAQ,IAAM,CAC7B,KAAK,cAAcA,EAAQ,iBAAiB,CAC9C,EACA,YAAa,CAAC,CAAE,QAAAA,CAAQ,IAAM,CAtJtC,IAAAC,GAuJUA,EAAA,KAAK,SAASD,EAAQ,iBAAiB,IAAvC,MAAAC,EAA0C,MAC5C,EACA,aAAc,CAAC,CAAE,QAAAD,CAAQ,IAAM,CAzJvC,IAAAC,GA0JUA,EAAA,KAAK,SAASD,EAAQ,iBAAiB,IAAvC,MAAAC,EAA0C,OAC5C,EACA,YAAa,CAAC,CAAE,QAAAD,EAAS,MAAAK,CAAM,IAAM,CA5J7C,IAAAJ,EA6JU,IAAMK,EAAID,GACVJ,EAAA,KAAK,SAASD,EAAQ,iBAAiB,IAAvC,MAAAC,EAA0C,KAAKK,EAAE,KACnD,EACA,kBAAmB,CAAC,CAAE,QAAAN,CAAQ,IAAM,CAhK5C,IAAAC,GAiKUA,EAAA,KAAK,SAASD,EAAQ,iBAAiB,IAAvC,MAAAC,EAA0C,KAAK,EACjD,EACA,gBAAiB,CAAC,CAAE,QAAAD,CAAQ,IAAM,CAChC,KAAK,oBAAoBA,EAAQ,iBAAiB,CACpD,EACA,uBAAwB,IAAM,CAC5B,KAAK,wBAAwB,CAC/B,EACA,2BAA4B,CAAC,CAAE,QAAAA,CAAQ,IAAM,CAC3C,KAAK,wBAAwB,EAC7B,KAAK,oBAAoBA,EAAQ,iBAAiB,CACpD,CACF,CACF,CAAC,EAED,KAAK,OAASO,EAAYT,CAAO,EAEjC,KAAK,OAAO,UAAWU,GAAa,CAClCC,EAAgCD,EAAS,QAAU,SAAS,CAC9D,CAAC,EAED,KAAK,OAAO,MAAM,EAElBE,EAAkB,CAChB,OAAQ,IAAM,KAAK,KAAK,EACxB,QAAS,IAAM,CACT,KAAK,OAAO,YAAY,EAAE,QAAU,WAAW,KAAK,MAAM,CAChE,EACA,OAAQ,IAAM,KAAK,KAAK,EACxB,WAAY,IAAM,KAAK,SAAS,EAChC,OAASC,GAAM,KAAK,KAAKA,CAAC,CAC5B,CAAC,CACH,CAMA,MAAa,CACN,KAAK,eACV,KAAK,OAAO,KAAK,CAAE,KAAM,MAAO,CAAC,CACnC,CAEA,OAAc,CACZ,KAAK,OAAO,KAAK,CAAE,KAAM,OAAQ,CAAC,CACpC,CAEA,iBAAwB,CAClB,KAAK,OAAO,YAAY,EAAE,QAAU,UACtC,KAAK,MAAM,EAEX,KAAK,KAAK,CAEd,CAEA,MAAa,CACE,KAAK,OAAO,YAAY,EACd,QAAQ,kBAAoB,GAClC,KAAK,QAAQ,QAE9B,KAAK,OAAO,KAAK,CAAE,KAAM,MAAO,CAAC,CACnC,CAEA,UAAiB,CACf,IAAMC,EAAK,KAAK,cAChB,GAAIA,GAAMA,EAAG,YAAc,EAAG,CAC5BA,EAAG,KAAK,CAAC,EACTA,EAAG,KAAK,EACR,MACF,CAEA,KAAK,OAAO,KAAK,CAAE,KAAM,UAAW,CAAC,CACvC,CAEA,UAAUC,EAAeC,EAAkB,GAAa,CAClDD,EAAQ,GAAKA,GAAS,KAAK,QAAQ,SACvC,KAAK,QACH,aAAaA,CAAK,qBAAqBC,CAAe,gBAAgB,KAAK,OAAO,YAAY,EAAE,KAAK,WAAW,KAAK,OAAO,YAAY,EAAE,QAAQ,iBAAiB,EACrK,EACA,KAAK,OAAO,KAAK,CAAE,KAAM,OAAQ,MAAAD,EAAO,gBAAAC,CAAgB,CAAC,EAC3D,CAEA,KAAKC,EAAoB,CACvB,KAAK,OAAO,KAAK,CAAE,KAAM,OAAQ,KAAAA,CAAK,CAAC,CACzC,CAEA,UAAUC,EAAsB,CAC9B,IAAMC,EAAU,KAAK,IAAI,EAAG,KAAK,IAAI,EAAGD,CAAM,CAAC,EAC/C,KAAK,QAAUC,EACf,QAAWf,KAAS,KAAK,QAASA,EAAM,UAAUe,CAAO,CAC3D,CAEA,gBAAgBC,EAAoB,CA7PtC,IAAAjB,EA8PI,IAAMgB,EAAU,KAAK,IAAI,EAAG,KAAK,IAAI,IAAMC,CAAI,CAAC,EAChD,KAAK,cAAgBD,GACrBhB,EAAA,KAAK,gBAAL,MAAAA,EAAoB,gBAAgBgB,GACpC,KAAK,wBAAwB,EAC7B,IAAME,EAAO,KAAK,OAAO,YAAY,EACjCA,EAAK,QAAU,WACjB,KAAK,oBAAoBA,EAAK,QAAQ,iBAAiB,CAE3D,CAEA,SAASvB,EAAaf,EAA2B,CAAC,EAAS,CACzD,IAAMgC,EAAQ,KAAK,QAAQ,OACrBO,EAAWvC,EAAQ,UAAa,CAAC,EACvC,KAAK,QAAQ,KACX,IAAIgB,EAAM,CACR,SAAUD,EACV,MAAAiB,EACA,MAAO,KACP,SAAUhC,EAAQ,SAClB,SAAAuC,CACF,CAAC,CACH,EACA,KAAK,OAAO,KAAK,CAAE,KAAM,WAAY,CAAC,CACxC,CAEA,YAAYP,EAAqB,CAC/B,GAAI,EAAAA,EAAQ,GAAKA,GAAS,KAAK,QAAQ,QACvC,MAAK,QAAQA,CAAK,EAAE,QAAQ,EAC5B,KAAK,QAAQ,OAAOA,EAAO,CAAC,EAC5B,QAASQ,EAAIR,EAAOQ,EAAI,KAAK,QAAQ,OAAQA,IAC1C,KAAK,QAAQA,CAAC,EAAmC,MAAQA,EAExD,KAAK,sBAAwBR,EAC/B,KAAK,oBAAsB,KAClB,KAAK,sBAAwB,MAAQ,KAAK,oBAAsBA,GACzE,KAAK,sBAEP,KAAK,OAAO,KAAK,CAAE,KAAM,eAAgB,MAAAA,CAAM,CAAC,EAClD,CAEA,oBAAoC,CAClC,OAAOS,EAAoB,CAC7B,CAEA,SAAgB,CACd,QAAWpB,KAAS,KAAK,QAASA,EAAM,QAAQ,EAChD,KAAK,QAAU,CAAC,EAChB,KAAK,OAAO,KAAK,CACnB,CAMA,IAAI,cAAsC,CApT5C,IAAAD,EAqTI,OAAOA,EAAA,KAAK,gBAAL,YAAAA,EAAoB,QAC7B,CAEA,IAAI,mBAA4B,CAC9B,OAAO,KAAK,OAAO,YAAY,EAAE,QAAQ,iBAC3C,CAEA,IAAI,QAA+B,CACjC,OAAO,KAAK,QAAQ,IAAKU,GAAMA,EAAE,OAAO,CAAC,CAC3C,CAEA,IAAI,WAAqB,CACvB,OAAO,KAAK,OAAO,YAAY,EAAE,QAAU,SAC7C,CAEA,IAAI,UAAoB,CACtB,OAAO,KAAK,OAAO,YAAY,EAAE,QAAU,QAC7C,CAEA,IAAI,QAAiB,CACnB,OAAO,KAAK,OACd,CAEA,IAAI,kBAA2B,CAC7B,OAAO,KAAK,iBACd,CAEA,IAAI,iBAAiBY,EAAe,CAClC,KAAK,kBAAoB,KAAK,IAAI,EAAGA,CAAK,EAC1C,IAAMJ,EAAO,KAAK,OAAO,YAAY,EACjCA,EAAK,QAAU,WACjB,KAAK,cAAcA,EAAK,QAAQ,iBAAiB,CAErD,CAEA,IAAI,cAAuB,CACzB,OAAO,KAAK,aACd,CAGA,IAAI,eAA+F,CACjG,IAAMA,EAAO,KAAK,OAAO,YAAY,EACrC,MAAO,CAAE,MAAOA,EAAK,MAAiB,QAASA,EAAK,OAAQ,CAC9D,CAMA,aAAajB,EAAoB,CAC/B,IAAMiB,EAAO,KAAK,OAAO,YAAY,EAMrC,GALA,KAAK,QACH,sBAAsBjB,EAAM,KAAK,eAAeiB,EAAK,KAAK,WAAWA,EAAK,QAAQ,iBAAiB,EACrG,EAGIjB,IAAU,KAAK,SAASiB,EAAK,QAAQ,iBAAiB,EAAG,OAE7D,KAAK,OAAO,KAAK,CAAE,KAAM,aAAc,CAAC,EACxC,IAAMK,EAAU,KAAK,OAAO,YAAY,EACxC,KAAK,QACH,oDAA+CA,EAAQ,KAAK,WAAWA,EAAQ,QAAQ,iBAAiB,EAC1G,CACF,CAEA,mBAAmBtB,EAAoB,CACrC,KAAK,OAAO,KAAK,CAAE,KAAM,eAAgB,MAAOA,EAAM,KAAM,CAAC,CAC/D,CAEA,WAAWuB,EAAuB,CA1XpC,IAAAxB,EA2XQwB,EAAK,QAAU,KAAK,OAAO,YAAY,EAAE,QAAQ,oBAChD,MAAMA,EAAK,QAAQ,GACtB,KAAK,8BAA8BA,EAAK,SAAUA,EAAK,YAAa,KAAK,aAAa,GAExFxB,EAAA,KAAK,cAAL,MAAAA,EAAA,UAAmBwB,GACrB,CAEA,QAAQC,EAAoB,CAlY9B,IAAAzB,GAmYIA,EAAA,KAAK,WAAL,MAAAA,EAAA,UAAgByB,EAClB,CAEA,eAAsB,CAtYxB,IAAAzB,EAuYI,KAAK,OAAO,KAAK,CAAE,KAAM,OAAQ,CAAC,GAClCA,EAAA,KAAK,iBAAL,MAAAA,EAAA,UACF,CAEA,eAAeC,EAAoB,CACjC,IAAMiB,EAAO,KAAK,OAAO,YAAY,EACjCjB,EAAM,QAAUiB,EAAK,QAAQ,mBACjC,KAAK,cAAcA,EAAK,QAAQ,iBAAiB,CACnD,CAEA,QAAQQ,EAAmB,CAjZ7B,IAAA1B,GAkZIA,EAAA,KAAK,WAAL,MAAAA,EAAA,UAAgB0B,EAClB,CAOQ,SAASd,EAAkC,CACjD,OAAO,KAAK,QAAQA,CAAK,CAC3B,CAEA,IAAY,eAAmC,CAC7C,OAAO,KAAK,QAAQ,KAAK,OAAO,YAAY,EAAE,QAAQ,iBAAiB,CACzE,CAEQ,cAAce,EAAyB,CAC7C,IAAMzB,EAAM,KAAK,SAASyB,CAAS,EAiBnC,GACEzB,GAAO,MACPA,EAAI,WACJA,EAAI,eAAiB,SACrBA,EAAI,uBAAyB,UAC7B,CACA,KAAK,QAAQ,iBAAiByB,CAAS,4DAAuD,EAC9F,MACF,CAOA,IAAMC,EACJ1B,GAAO,MACPA,EAAI,YACH,IAAM,CACL,IAAM2B,EAAY,MAAM3B,EAAI,QAAQ,EAAI,GAAK,KAAK,IAAIA,EAAI,SAAW,GAAK,EAAE,EAC5E,OAAOA,EAAI,YAAc2B,CAC3B,GAAG,EAECC,EAAQH,EAAY,KAAK,kBAAoB,EACnD,KAAK,QAAQ,iBAAiBA,CAAS,WAAWG,CAAK,eAAe,KAAK,QAAQ,MAAM,mBAAmBF,CAAiB,EAAE,EAC/H,QAASR,EAAIO,EAAY,EAAGP,EAAI,KAAK,QAAQ,QAAUA,EAAIU,EAAOV,IAAK,CACrE,IAAMV,EAAI,KAAK,QAAQU,CAAC,EACxB,GAAIA,EAAIO,EAAY,GAAKC,EAAmB,CAC1C,KAAK,QAAQ,kCAAkCR,CAAC,gBAAgBlB,EAAK,YAAY,QAAQ,CAAC,CAAC,qBAAqB,EAChH,MACF,CACA,GAAKQ,EAAE,eAKL,KAAK,QAAQ,wBAAwBU,CAAC,iBAAiB,MALlC,CACrB,KAAK,QAAQ,6CAA6CA,CAAC,EAAE,EAC7DV,EAAE,QAAQ,EACV,MACF,CAGF,CACF,CAEQ,yBAAgC,CACtC,GAAI,KAAK,sBAAwB,KAAM,OACvC,IAAMT,EAAQ,KAAK,SAAS,KAAK,mBAAmB,EAChDA,IACFA,EAAM,mBAAmB,EACzB,KAAK,QAAQ,4CAA4C,KAAK,mBAAmB,EAAE,GAErF,KAAK,oBAAsB,IAC7B,CAEQ,oBAAoB8B,EAAwB,CAClD,IAAMC,EAAMC,EAAgB,EAC5B,GAAI,CAACD,GAAO,KAAK,iBAAmB,aAAc,OAElD,IAAME,EAAYH,EAAW,EAC7B,GAAIG,GAAa,KAAK,QAAQ,OAAQ,OAEtC,IAAMC,EAAU,KAAK,QAAQJ,CAAQ,EAC/BK,EAAO,KAAK,QAAQF,CAAS,EAEnC,GACE,CAACC,EAAQ,gBACT,CAACC,EAAK,gBACN,KAAK,sBAAwBF,GAC7B,CAACC,EAAQ,UAET,OAEF,IAAME,EAAU,KAAK,qBAAqBF,CAAO,EAGjD,GAFIE,IAAY,MAEZA,EAAUL,EAAI,YAAc,IAAM,OAEtC,IAAMM,EAAeD,EAAUL,EAAI,YACnC,GAAIG,EAAQ,eAAiB,SAAWG,EAAejE,EAAwB,CAC7E,KAAK,QACH,qDAAgD0D,CAAQ,QAAQO,EAAa,QAAQ,CAAC,CAAC,8BAA8BjE,CAAsB,IAC7I,EACA,MACF,CAEA+D,EAAK,qBAAqBC,CAAO,EACjC,KAAK,QACH,wCAAwCH,CAAS,eAAeG,EAAQ,QAAQ,CAAC,CAAC,SAASA,EAAUL,EAAI,aAAa,QAAQ,CAAC,CAAC,sBAAsBG,EAAQ,YAAY,EAC5K,EACA,KAAK,oBAAsBD,CAC7B,CAEQ,qBAAqBjC,EAA6B,CACxD,IAAM+B,EAAMC,EAAgB,EAC5B,GAAI,CAACD,GAAO,CAAC/B,EAAM,eAAgB,OAAO,KAC1C,IAAMzB,EAAWyB,EAAM,SACvB,GAAI,MAAMzB,CAAQ,EAAG,OAAO,KAE5B,GAAIyB,EAAM,4BAA8B,KACtC,OAAOA,EAAM,0BAA4BzB,EAAW,KAAK,cAG3D,IAAM+D,GAAa/D,EAAWyB,EAAM,aAAe,KAAK,cACxD,OAAIsC,GAAa,EAAU,KACpBP,EAAI,YAAcO,CAC3B,CACF","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","t","createActor","time","clamped","v","vol","rate","oldRate","secondsFromEnd","dur","snap","promise","err","wasPlaying","offset","t0","t1","ctxRef","html5GainRef","savedVolume","fadeInSec","fadeNode","loop","remaining","preloadThreshold","throttle","fn","ms","last","args","now","MAX_SCHEDULE_LOOKAHEAD","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","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","rate","snap","metadata","i","resumeAudioContext","value","newSnap","info","error","msg","fromIndex","curBelowThreshold","threshold","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","timeUntilEnd","remaining"]}
|
package/package.json
CHANGED
package/src/Track.ts
CHANGED
|
@@ -149,6 +149,17 @@ export class Track {
|
|
|
149
149
|
// HTML5 Audio
|
|
150
150
|
this.audio = new Audio();
|
|
151
151
|
this.audio.preload = 'none';
|
|
152
|
+
// crossOrigin must be set BEFORE src for MediaElementAudioSourceNode to
|
|
153
|
+
// actually expose the element's audio to the AudioContext graph. Without
|
|
154
|
+
// it, even servers that DO send CORS headers result in the
|
|
155
|
+
// MediaElementSource being treated as cross-origin tainted, and the node
|
|
156
|
+
// outputs silence — manifesting as "HTML5 plays fine until WebAudio
|
|
157
|
+
// takes over, then suddenly audible". HTML5_ONLY mode skips this since
|
|
158
|
+
// it doesn't route through AudioContext; setting crossOrigin there
|
|
159
|
+
// would make non-CORS sources fail to load at all.
|
|
160
|
+
if (opts.queue.playbackMethod !== 'HTML5_ONLY') {
|
|
161
|
+
this.audio.crossOrigin = 'anonymous';
|
|
162
|
+
}
|
|
152
163
|
this.audio.src = this._trackUrl;
|
|
153
164
|
this.audio.volume = opts.queue.volume;
|
|
154
165
|
this.audio.controls = false;
|