@waveform-playlist/playout 10.1.2 → 10.3.0

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.js CHANGED
@@ -1599,6 +1599,11 @@ function createToneAdapter(options) {
1599
1599
  addTrackToPlayout(playout, track);
1600
1600
  playout.applyInitialSoloState();
1601
1601
  },
1602
+ removeTrack(trackId) {
1603
+ if (!playout) return;
1604
+ playout.removeTrack(trackId);
1605
+ playout.applyInitialSoloState();
1606
+ },
1602
1607
  play(startTime, endTime) {
1603
1608
  if (!playout) {
1604
1609
  console.warn(
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/TonePlayout.ts","../src/ToneTrack.ts","../src/fades.ts","../src/MidiToneTrack.ts","../src/SoundFontToneTrack.ts","../src/SoundFontCache.ts","../src/audioContext.ts","../src/mediaStreamSourceManager.ts","../src/TonePlayoutAdapter.ts"],"sourcesContent":["export { TonePlayout } from './TonePlayout';\nexport { ToneTrack } from './ToneTrack';\nexport { MidiToneTrack } from './MidiToneTrack';\nexport type { PlayableTrack, MidiClipInfo, MidiToneTrackOptions } from './MidiToneTrack';\nexport { SoundFontToneTrack } from './SoundFontToneTrack';\nexport type { SoundFontToneTrackOptions } from './SoundFontToneTrack';\nexport {\n SoundFontCache,\n timecentsToSeconds,\n getGeneratorValue,\n int16ToFloat32,\n calculatePlaybackRate,\n extractLoopAndEnvelope,\n} from './SoundFontCache';\nexport type { SoundFontSample, PlaybackRateParams, LoopAndEnvelopeParams } from './SoundFontCache';\nexport type { TonePlayoutOptions, EffectsFunction } from './TonePlayout';\nexport type { ToneTrackOptions, TrackEffectsFunction } from './ToneTrack';\n\n// Export global AudioContext manager\nexport {\n getGlobalContext,\n getGlobalAudioContext,\n getGlobalToneContext,\n resumeGlobalAudioContext,\n getGlobalAudioContextState,\n closeGlobalAudioContext,\n} from './audioContext';\n\n// Export MediaStreamSource manager\nexport {\n getMediaStreamSource,\n releaseMediaStreamSource,\n hasMediaStreamSource,\n} from './mediaStreamSourceManager';\n\n// Export fade utilities (pure functions re-exported from @waveform-playlist/core)\nexport { applyFadeIn, applyFadeOut, type FadeConfig, type FadeType } from './fades';\n\n// Export Tone.js-specific fade helper\nexport { getUnderlyingAudioParam } from './fades';\n\n// Export Tone.js adapter for engine integration\nexport { createToneAdapter } from './TonePlayoutAdapter';\nexport type { ToneAdapterOptions } from './TonePlayoutAdapter';\n","import {\n Volume,\n ToneAudioNode,\n getDestination,\n start,\n now,\n getTransport,\n getContext,\n BaseContext,\n} from 'tone';\nimport { ToneTrack, ToneTrackOptions } from './ToneTrack';\nimport { MidiToneTrack, MidiToneTrackOptions } from './MidiToneTrack';\nimport type { PlayableTrack } from './MidiToneTrack';\nimport { SoundFontToneTrack, SoundFontToneTrackOptions } from './SoundFontToneTrack';\n\nexport type EffectsFunction = (\n masterGainNode: Volume,\n destination: ToneAudioNode,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface TonePlayoutOptions {\n tracks?: ToneTrack[];\n masterGain?: number;\n effects?: EffectsFunction;\n}\n\nexport class TonePlayout {\n private tracks: Map<string, PlayableTrack> = new Map();\n private masterVolume: Volume;\n private isInitialized = false;\n private soloedTracks: Set<string> = new Set();\n private manualMuteState: Map<string, boolean> = new Map();\n private effectsCleanup?: () => void;\n private onPlaybackCompleteCallback?: () => void;\n private _completionEventId: number | null = null;\n private _loopHandler: (() => void) | null = null;\n private _loopEnabled = false;\n private _loopStart = 0;\n private _loopEnd = 0;\n\n constructor(options: TonePlayoutOptions = {}) {\n this.masterVolume = new Volume(this.gainToDb(options.masterGain ?? 1));\n\n // Setup effects chain if provided, otherwise connect directly to destination\n if (options.effects) {\n const cleanup = options.effects(this.masterVolume, getDestination(), false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.masterVolume.toDestination();\n }\n\n if (options.tracks) {\n options.tracks.forEach((track) => {\n this.tracks.set(track.id, track);\n this.manualMuteState.set(track.id, track.muted);\n });\n }\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n private clearCompletionEvent(): void {\n if (this._completionEventId !== null) {\n try {\n getTransport().clear(this._completionEventId);\n } catch (err) {\n console.warn('[waveform-playlist] Error clearing Transport completion event:', err);\n }\n this._completionEventId = null;\n }\n }\n\n async init(): Promise<void> {\n if (this.isInitialized) return;\n\n await start();\n this.isInitialized = true;\n }\n\n addTrack(trackOptions: ToneTrackOptions): ToneTrack {\n // Ensure tracks connect to master volume instead of destination\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const toneTrack = new ToneTrack(optionsWithDestination);\n this.tracks.set(toneTrack.id, toneTrack);\n this.manualMuteState.set(toneTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(toneTrack.id);\n }\n return toneTrack;\n }\n\n addMidiTrack(trackOptions: MidiToneTrackOptions): MidiToneTrack {\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const midiTrack = new MidiToneTrack(optionsWithDestination);\n this.tracks.set(midiTrack.id, midiTrack);\n this.manualMuteState.set(midiTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(midiTrack.id);\n }\n return midiTrack;\n }\n\n addSoundFontTrack(trackOptions: SoundFontToneTrackOptions): SoundFontToneTrack {\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const sfTrack = new SoundFontToneTrack(optionsWithDestination);\n this.tracks.set(sfTrack.id, sfTrack);\n this.manualMuteState.set(sfTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(sfTrack.id);\n }\n return sfTrack;\n }\n\n /**\n * Apply solo muting after all tracks have been added.\n * Call this after adding all tracks to ensure solo logic is applied correctly.\n */\n applyInitialSoloState(): void {\n this.updateSoloMuting();\n }\n\n removeTrack(trackId: string): void {\n const track = this.tracks.get(trackId);\n if (track) {\n track.dispose();\n this.tracks.delete(trackId);\n this.manualMuteState.delete(trackId);\n this.soloedTracks.delete(trackId);\n }\n }\n\n getTrack(trackId: string): PlayableTrack | undefined {\n return this.tracks.get(trackId);\n }\n\n play(when?: number, offset?: number, duration?: number): void {\n if (!this.isInitialized) {\n throw new Error('[waveform-playlist] TonePlayout not initialized. Call init() first.');\n }\n\n const startTime = when ?? now();\n const transport = getTransport();\n\n this.clearCompletionEvent();\n\n const transportOffset = offset ?? 0;\n this.tracks.forEach((track) => {\n track.cancelFades();\n track.prepareFades(startTime, transportOffset);\n });\n\n // Schedule duration-limited stop via Transport\n if (duration !== undefined) {\n this._completionEventId = transport.scheduleOnce(() => {\n this._completionEventId = null;\n try {\n this.onPlaybackCompleteCallback?.();\n } catch (err) {\n console.warn('[waveform-playlist] Error in playback completion callback:', err);\n }\n }, transportOffset + duration);\n }\n\n // Start Transport — triggers schedule() callbacks for clips at/after offset\n try {\n // Stop all active native sources before restarting to prevent layered audio.\n // Native AudioBufferSourceNodes don't respond to Transport state changes.\n if (transport.state !== 'stopped') {\n transport.stop();\n }\n this.tracks.forEach((track) => track.stopAllSources());\n\n // Set loop boundaries BEFORE enabling loop. _processTick checks\n // `ticks >= _loopEnd` every tick; _loopEnd defaults to 0.\n transport.loopStart = this._loopStart;\n transport.loopEnd = this._loopEnd;\n transport.loop = this._loopEnabled;\n\n // Set schedule guard BEFORE transport.start(). Ghost ticks from stale\n // Clock._lastUpdate can fire schedule callbacks at past positions;\n // the guard suppresses callbacks for clips before the play offset\n // (those are handled by startMidClipSources below).\n this.tracks.forEach((track) => track.setScheduleGuardOffset(transportOffset));\n\n if (offset !== undefined) {\n transport.start(startTime, offset);\n } else {\n transport.start(startTime);\n }\n\n // Advance Clock._lastUpdate past the stop/start boundary.\n //\n // After stop/start cycles, _lastUpdate is stale (set by the previous\n // context tick, before our stop/start). The next Clock._loop() processes\n // ticks from [_lastUpdate, now()]. In the gap [_lastUpdate, startTime),\n // the TickSource is still \"started\" from the PREVIOUS play cycle with\n // its old tick offset. Those accumulated ticks can exceed _loopEnd,\n // causing _processTick to wrap immediately to loopStart.\n //\n // By advancing _lastUpdate to startTime, we skip the stale range.\n // The next Clock._loop() only processes [startTime, now()] — ticks\n // from the current play cycle with the correct offset.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tone.js private internal, see CLAUDE.md ghost tick fix\n (transport as any)._clock._lastUpdate = startTime;\n\n // Start sources for clips that span the current Transport position.\n // Transport.schedule() only fires for clips at/after the offset;\n // clips whose start time is before the offset need manual creation.\n this.tracks.forEach((track) => {\n track.startMidClipSources(transportOffset, startTime);\n });\n } catch (err) {\n // Clean up scheduled events since Transport failed to start\n this.clearCompletionEvent();\n this.tracks.forEach((track) => track.cancelFades());\n console.warn(\n '[waveform-playlist] Transport.start() failed. Audio playback could not begin.',\n err\n );\n throw err;\n }\n }\n\n pause(): void {\n const transport = getTransport();\n try {\n transport.pause();\n } catch (err) {\n console.warn('[waveform-playlist] Transport.pause() failed:', err);\n }\n // Native AudioBufferSourceNodes ignore Transport state changes —\n // they must be explicitly stopped.\n this.tracks.forEach((track) => track.stopAllSources());\n this.tracks.forEach((track) => track.cancelFades());\n this.clearCompletionEvent();\n }\n\n stop(): void {\n const transport = getTransport();\n try {\n transport.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Transport.stop() failed:', err);\n }\n // Remove loop handler before stopping sources. Prevents any deferred\n // loop event from creating new sources via startMidClipSources() after\n // stopAllSources() runs. Defense-in-depth — setLoop(false) should\n // already have removed it, but stop() must be self-contained.\n if (this._loopHandler) {\n try {\n transport.off('loop', this._loopHandler);\n } catch (err) {\n console.warn('[waveform-playlist] Error removing loop handler:', err);\n }\n this._loopHandler = null;\n }\n // Stop all native sources explicitly\n this.tracks.forEach((track) => {\n try {\n track.stopAllSources();\n } catch (err) {\n console.warn(`[waveform-playlist] Error stopping sources for track \"${track.id}\":`, err);\n }\n });\n this.tracks.forEach((track) => {\n try {\n track.cancelFades();\n } catch (err) {\n console.warn(`[waveform-playlist] Error canceling fades for track \"${track.id}\":`, err);\n }\n });\n this.clearCompletionEvent();\n }\n\n setMasterGain(gain: number): void {\n this.masterVolume.volume.value = this.gainToDb(gain);\n }\n\n setSolo(trackId: string, soloed: boolean): void {\n const track = this.tracks.get(trackId);\n if (track) {\n track.setSolo(soloed);\n if (soloed) {\n this.soloedTracks.add(trackId);\n } else {\n this.soloedTracks.delete(trackId);\n }\n this.updateSoloMuting();\n }\n }\n\n private updateSoloMuting(): void {\n const hasSoloedTracks = this.soloedTracks.size > 0;\n\n this.tracks.forEach((track, id) => {\n if (hasSoloedTracks) {\n if (!this.soloedTracks.has(id)) {\n track.setMute(true);\n } else {\n const manuallyMuted = this.manualMuteState.get(id) ?? false;\n track.setMute(manuallyMuted);\n }\n } else {\n const manuallyMuted = this.manualMuteState.get(id) ?? false;\n track.setMute(manuallyMuted);\n }\n });\n }\n\n setMute(trackId: string, muted: boolean): void {\n const track = this.tracks.get(trackId);\n if (track) {\n this.manualMuteState.set(trackId, muted);\n track.setMute(muted);\n }\n }\n\n setLoop(enabled: boolean, loopStart: number, loopEnd: number): void {\n // Update cached state first — play() uses these values to configure\n // the Transport on next start, so they must reflect the caller's intent\n // regardless of whether Transport property setting succeeds.\n this._loopEnabled = enabled;\n this._loopStart = loopStart;\n this._loopEnd = loopEnd;\n\n const transport = getTransport();\n try {\n // Set boundaries BEFORE enabling loop. Tone.js's _processTick checks\n // `ticks >= _loopEnd` on every tick. If we set transport.loop = true\n // first, a tick could fire before loopEnd is updated, seeing the stale\n // _loopEnd value (0 from Transport default) and wrapping immediately.\n transport.loopStart = loopStart;\n transport.loopEnd = loopEnd;\n transport.loop = enabled;\n } catch (err) {\n console.warn('[waveform-playlist] Error configuring Transport loop:', err);\n return;\n }\n\n if (enabled && !this._loopHandler) {\n this._loopHandler = () => {\n // On loop boundary: stop old sources, re-schedule fades, start mid-clip sources.\n // Event ordering in Transport's tick processing (Tone.js 15.x _processTick):\n // loopEnd → ticks reset → loopStart → loop → forEachAtTime(ticks)\n // Our loop handler fires BEFORE schedule callbacks, so:\n // 1. stopAllSources + cancelFades — clean slate\n // 2. startMidClipSources — for clips spanning loopStart boundary\n // 3. prepareFades — fresh fade envelopes\n // Then Transport fires schedule callbacks for clips at/after loopStart.\n const currentTime = now();\n this.tracks.forEach((track) => {\n try {\n track.stopAllSources();\n track.cancelFades();\n track.setScheduleGuardOffset(this._loopStart);\n track.startMidClipSources(this._loopStart, currentTime);\n track.prepareFades(currentTime, this._loopStart);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error re-scheduling track \"${track.id}\" on loop:`,\n err\n );\n }\n });\n };\n transport.on('loop', this._loopHandler);\n } else if (!enabled && this._loopHandler) {\n transport.off('loop', this._loopHandler);\n this._loopHandler = null;\n }\n }\n\n getCurrentTime(): number {\n return getTransport().seconds;\n }\n\n seekTo(time: number): void {\n getTransport().seconds = time;\n }\n\n dispose(): void {\n // Stop Transport and all active sources before disposing.\n // Without this, the global Transport singleton keeps firing scheduled\n // callbacks and native AudioBufferSourceNodes continue playing through\n // the shared AudioContext (e.g., during Docusaurus client-side navigation).\n try {\n this.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Error stopping Transport during dispose:', err);\n }\n\n this.tracks.forEach((track) => {\n try {\n track.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing track \"${track.id}\":`, err);\n }\n });\n this.tracks.clear();\n\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn('[waveform-playlist] Error during master effects cleanup:', err);\n }\n }\n\n try {\n this.masterVolume.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing master volume:', err);\n }\n }\n\n get context(): BaseContext {\n return getContext();\n }\n\n get sampleRate(): number {\n return getContext().sampleRate;\n }\n\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n}\n","import {\n Volume,\n Gain,\n Panner,\n ToneAudioNode,\n getDestination,\n getTransport,\n getContext,\n} from 'tone';\nimport { Track, type Fade } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, getUnderlyingAudioParam } from './fades';\n\nexport type TrackEffectsFunction = (\n graphEnd: Gain,\n masterGainNode: ToneAudioNode,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface ClipInfo {\n buffer: AudioBuffer;\n startTime: number; // When this clip starts in the track timeline (seconds)\n duration: number; // How long this clip plays (seconds)\n offset: number; // Where to start playing within the buffer (seconds)\n fadeIn?: Fade;\n fadeOut?: Fade;\n gain: number; // Clip-level gain\n}\n\nexport interface ToneTrackOptions {\n buffer?: AudioBuffer; // Legacy: single buffer (deprecated, use clips instead)\n clips?: ClipInfo[]; // Modern: array of clips\n track: Track;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n}\n\n/** Per-clip scheduling info and audio nodes */\ninterface ScheduledClip {\n clipInfo: ClipInfo;\n fadeGainNode: GainNode; // Native GainNode for per-clip fade envelope\n scheduleId: number; // Transport.schedule() event ID\n}\n\nexport class ToneTrack {\n private scheduledClips: ScheduledClip[];\n private activeSources: Set<AudioBufferSourceNode> = new Set();\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n // Guard against ghost tick schedule callbacks. After stop/start cycles with\n // loops, stale Clock._lastUpdate causes ticks from the previous cycle to fire\n // Transport.schedule() callbacks at past positions (e.g., time 0 clips fire\n // when starting at offset 5s). Clips before this offset are handled by\n // startMidClipSources(); schedule callbacks should only create sources for\n // clips at/after this offset.\n private _scheduleGuardOffset = 0;\n\n constructor(options: ToneTrackOptions) {\n this.track = options.track;\n\n // Create shared track-level Tone.js nodes\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n // Tone.js Panner defaults to channelCount: 1 + channelCountMode: 'explicit',\n // which forces stereo→mono downmix (1/√2 attenuation) before panning.\n // Override to channelCount: 2 to preserve stereo recordings.\n this.panNode = new Panner({ pan: options.track.stereoPan, channelCount: 2 });\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n\n // Chain shared Tone.js nodes: Volume → Pan → MuteGain\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create clips array - support both legacy single buffer and modern clips array\n const clipInfos: ClipInfo[] =\n options.clips ||\n (options.buffer\n ? [\n {\n buffer: options.buffer,\n startTime: 0,\n duration: options.buffer.duration,\n offset: 0,\n fadeIn: options.track.fadeIn,\n fadeOut: options.track.fadeOut,\n gain: 1,\n },\n ]\n : []);\n\n const transport = getTransport();\n const rawContext = getContext().rawContext as AudioContext;\n\n // Get the native AudioNode input of the Volume for native→Tone connection.\n // Volume.input is a Tone.js Gain<\"decibels\"> whose .input is the native GainNode.\n // Cast through unknown since Gain<\"decibels\"> and Gain<\"gain\"> don't overlap.\n const volumeNativeInput = (this.volumeNode.input as unknown as Gain).input;\n\n // Schedule each clip via Transport.schedule() with native AudioBufferSourceNode\n this.scheduledClips = clipInfos.map((clipInfo) => {\n // Native GainNode for per-clip fade envelope — created once, reused across play cycles\n const fadeGainNode = rawContext.createGain();\n fadeGainNode.gain.value = clipInfo.gain;\n fadeGainNode.connect(volumeNativeInput);\n\n // Schedule a permanent Transport event at the clip's absolute timeline position.\n // This callback fires on every play and every loop iteration when Transport\n // passes this point.\n const absTransportTime = this.track.startTime + clipInfo.startTime;\n const scheduleId = transport.schedule((audioContextTime: number) => {\n // Guard: ghost ticks from stale Clock._lastUpdate can fire this callback\n // at past positions (see Tone.js #1419). Clips before the play/loop offset\n // are already handled by startMidClipSources().\n if (absTransportTime < this._scheduleGuardOffset) {\n return;\n }\n this.startClipSource(clipInfo, fadeGainNode, audioContextTime);\n }, absTransportTime);\n\n return { clipInfo, fadeGainNode, scheduleId };\n });\n }\n\n /**\n * Create and start an AudioBufferSourceNode for a clip.\n * Sources are one-shot: each play or loop iteration creates a fresh one.\n */\n private startClipSource(\n clipInfo: ClipInfo,\n fadeGainNode: GainNode,\n audioContextTime: number,\n bufferOffset?: number,\n playDuration?: number\n ): void {\n const rawContext = getContext().rawContext as AudioContext;\n const source = rawContext.createBufferSource();\n source.buffer = clipInfo.buffer;\n source.connect(fadeGainNode);\n\n const offset = bufferOffset ?? clipInfo.offset;\n const duration = playDuration ?? clipInfo.duration;\n\n try {\n source.start(audioContextTime, offset, duration);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start source on track \"${this.id}\" ` +\n `(time=${audioContextTime}, offset=${offset}, duration=${duration}):`,\n err\n );\n source.disconnect();\n return;\n }\n\n this.activeSources.add(source);\n source.onended = () => {\n this.activeSources.delete(source);\n };\n }\n\n /**\n * Set the schedule guard offset. Schedule callbacks for clips before this\n * offset are suppressed (already handled by startMidClipSources).\n * Must be called before transport.start() and in the loop handler.\n */\n setScheduleGuardOffset(offset: number): void {\n this._scheduleGuardOffset = offset;\n }\n\n /**\n * Start sources for clips that span the given Transport position.\n * Used for mid-playback seeking and loop boundary handling where\n * Transport.schedule() callbacks have already passed.\n *\n * Uses strict < for absClipStart to avoid double-creation with\n * schedule callbacks at exact Transport position (e.g., loopStart).\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo, fadeGainNode } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n // Only handle clips that started before the transport position\n // but haven't ended yet (i.e., the transport is \"inside\" the clip)\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n const elapsed = transportOffset - absClipStart;\n const adjustedOffset = clipInfo.offset + elapsed;\n const remainingDuration = clipInfo.duration - elapsed;\n this.startClipSource(\n clipInfo,\n fadeGainNode,\n audioContextTime,\n adjustedOffset,\n remainingDuration\n );\n }\n }\n }\n\n /**\n * Stop all active AudioBufferSourceNodes and clear the set.\n * Native AudioBufferSourceNodes ignore Transport state changes —\n * they must be explicitly stopped.\n */\n stopAllSources(): void {\n this.activeSources.forEach((source) => {\n try {\n source.stop();\n } catch (err) {\n console.warn(`[waveform-playlist] Error stopping source on track \"${this.id}\":`, err);\n }\n });\n this.activeSources.clear();\n }\n\n /**\n * Schedule fade envelopes for a clip at the given AudioContext time.\n * Uses native GainNode.gain (AudioParam) directly — no _param workaround needed.\n */\n private scheduleFades(\n scheduled: ScheduledClip,\n clipStartTime: number,\n clipOffset: number = 0\n ): void {\n const { clipInfo, fadeGainNode } = scheduled;\n const audioParam = fadeGainNode.gain;\n\n // Cancel any previous automation\n audioParam.cancelScheduledValues(0);\n\n // Calculate how much of the clip we're skipping (for seeking)\n const skipTime = clipOffset - clipInfo.offset;\n\n // Apply fade in if it exists and we haven't skipped past it\n if (clipInfo.fadeIn && skipTime < clipInfo.fadeIn.duration) {\n const fadeInDuration = clipInfo.fadeIn.duration;\n\n if (skipTime <= 0) {\n applyFadeIn(\n audioParam,\n clipStartTime,\n fadeInDuration,\n clipInfo.fadeIn.type || 'linear',\n 0,\n clipInfo.gain\n );\n } else {\n const remainingFadeDuration = fadeInDuration - skipTime;\n const fadeProgress = skipTime / fadeInDuration;\n const startValue = clipInfo.gain * fadeProgress;\n applyFadeIn(\n audioParam,\n clipStartTime,\n remainingFadeDuration,\n clipInfo.fadeIn.type || 'linear',\n startValue,\n clipInfo.gain\n );\n }\n } else {\n audioParam.setValueAtTime(clipInfo.gain, clipStartTime);\n }\n\n // Apply fade out if it exists\n if (clipInfo.fadeOut) {\n const fadeOutStart = clipInfo.duration - clipInfo.fadeOut.duration;\n const fadeOutStartInClip = fadeOutStart - skipTime;\n\n if (fadeOutStartInClip > 0) {\n const absoluteFadeOutStart = clipStartTime + fadeOutStartInClip;\n applyFadeOut(\n audioParam,\n absoluteFadeOutStart,\n clipInfo.fadeOut.duration,\n clipInfo.fadeOut.type || 'linear',\n clipInfo.gain,\n 0\n );\n } else if (fadeOutStartInClip > -clipInfo.fadeOut.duration) {\n const elapsedFadeOut = -fadeOutStartInClip;\n const remainingFadeDuration = clipInfo.fadeOut.duration - elapsedFadeOut;\n const fadeProgress = elapsedFadeOut / clipInfo.fadeOut.duration;\n const startValue = clipInfo.gain * (1 - fadeProgress);\n applyFadeOut(\n audioParam,\n clipStartTime,\n remainingFadeDuration,\n clipInfo.fadeOut.type || 'linear',\n startValue,\n 0\n );\n }\n }\n }\n\n /**\n * Prepare fade envelopes for all clips based on Transport offset.\n * Called before Transport.start() to schedule fades at correct AudioContext times.\n */\n prepareFades(when: number, transportOffset: number): void {\n this.scheduledClips.forEach((scheduled) => {\n const absClipStart = this.track.startTime + scheduled.clipInfo.startTime;\n const absClipEnd = absClipStart + scheduled.clipInfo.duration;\n\n if (transportOffset >= absClipEnd) return; // clip already finished\n\n if (transportOffset >= absClipStart) {\n // Mid-clip: playing now\n const clipOffset = transportOffset - absClipStart + scheduled.clipInfo.offset;\n this.scheduleFades(scheduled, when, clipOffset);\n } else {\n // Clip starts later\n const delay = absClipStart - transportOffset;\n this.scheduleFades(scheduled, when + delay, scheduled.clipInfo.offset);\n }\n });\n }\n\n /**\n * Cancel all scheduled fade automation and reset to nominal gain.\n * Called on pause/stop to prevent stale fade envelopes.\n */\n cancelFades(): void {\n this.scheduledClips.forEach(({ fadeGainNode, clipInfo }) => {\n const audioParam = fadeGainNode.gain;\n audioParam.cancelScheduledValues(0);\n audioParam.setValueAtTime(clipInfo.gain, 0);\n });\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n // Use setValueAtTime on the raw AudioParam to ensure the value is applied\n // even when the AudioContext is suspended. Setting .gain.value on the Tone.js\n // Signal wrapper doesn't propagate to the underlying AudioParam until the\n // context resumes, causing a brief audio glitch (e.g., all tracks audible\n // before solo muting takes effect).\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n const transport = getTransport();\n\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(`[waveform-playlist] Error during track \"${this.id}\" effects cleanup:`, err);\n }\n }\n\n this.stopAllSources();\n\n // Clear Transport schedule events and disconnect native fade gain nodes\n this.scheduledClips.forEach((scheduled, index) => {\n try {\n transport.clear(scheduled.scheduleId);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error clearing schedule ${index} on track \"${this.id}\":`,\n err\n );\n }\n try {\n scheduled.fadeGainNode.disconnect();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disconnecting fadeGain ${index} on track \"${this.id}\":`,\n err\n );\n }\n });\n\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing volumeNode on track \"${this.id}\":`, err);\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing panNode on track \"${this.id}\":`, err);\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing muteGain on track \"${this.id}\":`, err);\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get buffer(): AudioBuffer {\n return this.scheduledClips[0]?.clipInfo.buffer;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","/**\n * Fade utilities — re-exports from @waveform-playlist/core\n * plus Tone.js-specific helpers\n */\n\n// Re-export all pure fade utilities from core\nexport {\n linearCurve,\n exponentialCurve,\n sCurveCurve,\n logarithmicCurve,\n generateCurve,\n applyFadeIn,\n applyFadeOut,\n} from '@waveform-playlist/core';\n\nexport type { FadeType, FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Access the underlying Web Audio AudioParam from a Tone.js Signal/Param wrapper.\n *\n * Tone.js wraps native AudioParam in its Signal class, but sometimes we need\n * direct access to the raw AudioParam for setValueAtTime/cancelScheduledValues\n * (e.g., when the AudioContext is suspended and Tone.js Signal doesn't propagate).\n *\n * This uses `_param` which is a private Tone.js 15.x internal.\n * Pin the Tone.js version carefully if upgrading.\n *\n * @param signal - A Tone.js Signal or Param wrapper (e.g., `gain.gain`)\n * @returns The underlying AudioParam, or undefined if not found\n */\nlet hasWarned = false;\n\nexport function getUnderlyingAudioParam(signal: unknown): AudioParam | undefined {\n const param = (signal as { _param?: AudioParam })._param;\n if (!param && !hasWarned) {\n hasWarned = true;\n console.warn(\n '[waveform-playlist] Unable to access Tone.js internal _param. ' +\n 'This likely means the Tone.js version is incompatible. ' +\n 'Mute scheduling may not work correctly.'\n );\n }\n return param;\n}\n","import {\n Volume,\n Gain,\n Panner,\n PolySynth,\n Synth,\n MembraneSynth,\n MetalSynth,\n NoiseSynth,\n Part,\n ToneAudioNode,\n getDestination,\n getContext,\n} from 'tone';\nimport type { SynthOptions } from 'tone';\nimport { Track } from '@waveform-playlist/core';\nimport type { MidiNoteData } from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam } from './fades';\nimport type { TrackEffectsFunction } from './ToneTrack';\n\n/**\n * Shared interface for tracks managed by TonePlayout.\n * Both ToneTrack (audio) and MidiToneTrack (MIDI) implement this,\n * allowing TonePlayout to manage them uniformly.\n */\nexport interface PlayableTrack {\n id: string;\n startTime: number;\n muted: boolean;\n duration: number;\n stopAllSources(): void;\n startMidClipSources(offset: number, time: number): void;\n setScheduleGuardOffset(offset: number): void;\n prepareFades(when: number, offset: number): void;\n cancelFades(): void;\n setVolume(gain: number): void;\n setPan(pan: number): void;\n setMute(muted: boolean): void;\n setSolo(soloed: boolean): void;\n dispose(): void;\n}\n\nexport interface MidiClipInfo {\n notes: MidiNoteData[];\n startTime: number; // When this clip starts relative to track start (seconds)\n duration: number; // Clip duration (seconds)\n offset: number; // Trim offset into the MIDI data (seconds)\n}\n\nexport interface MidiToneTrackOptions {\n clips: MidiClipInfo[];\n track: Track;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n synthOptions?: Partial<SynthOptions>;\n}\n\n/**\n * Categorize GM percussion note numbers into synth types.\n * See: https://www.midi.org/specifications-old/item/gm-level-1-sound-set\n */\ntype DrumCategory = 'kick' | 'snare' | 'cymbal' | 'tom';\n\nfunction getDrumCategory(midiNote: number): DrumCategory {\n // Bass drums\n if (midiNote === 35 || midiNote === 36) return 'kick';\n // Snare, side stick, clap\n if (midiNote >= 37 && midiNote <= 40) return 'snare';\n // Toms (low floor tom through high tom)\n if (\n midiNote === 41 ||\n midiNote === 43 ||\n midiNote === 45 ||\n midiNote === 47 ||\n midiNote === 48 ||\n midiNote === 50\n )\n return 'tom';\n // Hi-hats, cymbals, bells, tambourine, cowbell, etc.\n return 'cymbal';\n}\n\n/** Per-clip scheduling info */\ninterface ScheduledMidiClip {\n clipInfo: MidiClipInfo;\n part: Part;\n}\n\n/**\n * MIDI track that always creates both melodic and percussion synths.\n * Per-note routing uses the `channel` field on each MidiNoteData:\n * channel 9 → percussion synths, all others → melodic PolySynth.\n * This enables flattened tracks (mixed channels) to play correctly.\n */\nexport class MidiToneTrack implements PlayableTrack {\n private scheduledClips: ScheduledMidiClip[];\n // Melodic synth — always created\n private synth: PolySynth;\n // Percussion synths — always created (PolySynth wrappers for polyphony)\n private kickSynth: PolySynth<MembraneSynth>;\n private snareSynth: NoiseSynth; // No pitch param, can't wrap in PolySynth\n private cymbalSynth: PolySynth<MetalSynth>;\n private tomSynth: PolySynth<MembraneSynth>;\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n\n constructor(options: MidiToneTrackOptions) {\n this.track = options.track;\n\n // Create shared track-level Tone.js nodes (same chain as ToneTrack)\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n this.panNode = new Panner(options.track.stereoPan);\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Melodic: PolySynth with basic Synth voice\n this.synth = new PolySynth(Synth, options.synthOptions);\n this.synth.connect(this.volumeNode);\n\n // Percussion: PolySynth wrappers for polyphonic playback\n this.kickSynth = new PolySynth(MembraneSynth, {\n voice: MembraneSynth,\n options: {\n pitchDecay: 0.05,\n octaves: 6,\n envelope: { attack: 0.001, decay: 0.4, sustain: 0, release: 0.1 },\n },\n } as never);\n this.snareSynth = new NoiseSynth({\n noise: { type: 'white' },\n envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.05 },\n });\n this.cymbalSynth = new PolySynth(MetalSynth, {\n voice: MetalSynth,\n options: {\n envelope: { attack: 0.001, decay: 0.3, release: 0.1 },\n harmonicity: 5.1,\n modulationIndex: 32,\n resonance: 4000,\n octaves: 1.5,\n },\n } as never);\n this.tomSynth = new PolySynth(MembraneSynth, {\n voice: MembraneSynth,\n options: {\n pitchDecay: 0.08,\n octaves: 4,\n envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.1 },\n },\n } as never);\n\n this.kickSynth.connect(this.volumeNode);\n this.snareSynth.connect(this.volumeNode);\n this.cymbalSynth.connect(this.volumeNode);\n this.tomSynth.connect(this.volumeNode);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create a Tone.Part for each clip, scheduling notes relative to Transport\n this.scheduledClips = options.clips.map((clipInfo) => {\n // Filter notes within the clip's visible window (after trim offset)\n const visibleNotes = clipInfo.notes.filter((note) => {\n const noteEnd = note.time + note.duration;\n // Note must start before clip ends and end after clip's trim offset\n return note.time < clipInfo.offset + clipInfo.duration && noteEnd > clipInfo.offset;\n });\n\n // Create Part events with absolute Transport times\n const absClipStart = this.track.startTime + clipInfo.startTime;\n\n const partEvents = visibleNotes.map((note) => {\n // Adjust note timing relative to clip's trim offset\n const adjustedTime = note.time - clipInfo.offset;\n // Clamp to clip boundaries\n const clampedStart = Math.max(0, adjustedTime);\n const clampedDuration = Math.min(\n note.duration - Math.max(0, clipInfo.offset - note.time),\n clipInfo.duration - clampedStart\n );\n\n return {\n time: absClipStart + clampedStart,\n note: note.name,\n midi: note.midi,\n duration: Math.max(0, clampedDuration),\n velocity: note.velocity,\n channel: note.channel,\n };\n });\n\n const part = new Part((time, event) => {\n if (event.duration > 0) {\n this.triggerNote(\n event.midi,\n event.note,\n event.duration,\n time,\n event.velocity,\n event.channel\n );\n }\n }, partEvents);\n\n // Part starts automatically with Transport\n part.start(0);\n\n return { clipInfo, part };\n });\n }\n\n /**\n * Trigger a note using the appropriate synth.\n * Routes per-note: channel 9 → percussion synths, others → melodic PolySynth.\n */\n private triggerNote(\n midiNote: number,\n noteName: string,\n duration: number,\n time: number,\n velocity: number,\n channel?: number\n ): void {\n if (channel === 9) {\n const category = getDrumCategory(midiNote);\n switch (category) {\n case 'kick':\n this.kickSynth.triggerAttackRelease('C1', duration, time, velocity);\n break;\n case 'snare':\n // NoiseSynth is monophonic — wrap in try-catch for rare overlaps\n try {\n this.snareSynth.triggerAttackRelease(duration, time, velocity);\n } catch (err) {\n console.warn(\n '[waveform-playlist] Snare overlap — previous hit still decaying, skipped:',\n err\n );\n }\n break;\n case 'tom': {\n const tomPitches: Record<number, string> = {\n 41: 'G1',\n 43: 'A1',\n 45: 'C2',\n 47: 'D2',\n 48: 'E2',\n 50: 'G2',\n };\n this.tomSynth.triggerAttackRelease(\n tomPitches[midiNote] || 'C2',\n duration,\n time,\n velocity\n );\n break;\n }\n case 'cymbal':\n // PolySynth requires a note arg; MetalSynth uses it as fundamental frequency.\n // 'C4' provides a reasonable metallic timbre for all cymbal/hi-hat hits.\n this.cymbalSynth.triggerAttackRelease('C4', duration, time, velocity);\n break;\n }\n } else {\n this.synth.triggerAttackRelease(noteName, duration, time, velocity);\n }\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n /**\n * No-op for MIDI — schedule guard is for AudioBufferSourceNode ghost tick prevention.\n * Tone.Part handles its own scheduling relative to Transport.\n */\n setScheduleGuardOffset(_offset: number): void {\n // No-op: Tone.Part handles scheduling internally\n }\n\n /**\n * For MIDI, mid-clip sources are notes that should already be sounding.\n * We trigger them with their remaining duration.\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n // Find notes that should be currently sounding\n for (const note of clipInfo.notes) {\n const adjustedTime = note.time - clipInfo.offset;\n const noteAbsStart = absClipStart + Math.max(0, adjustedTime);\n const noteAbsEnd = noteAbsStart + note.duration;\n\n if (noteAbsStart < transportOffset && noteAbsEnd > transportOffset) {\n const remainingDuration = noteAbsEnd - transportOffset;\n try {\n this.triggerNote(\n note.midi,\n note.name,\n remainingDuration,\n audioContextTime,\n note.velocity,\n note.channel\n );\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start mid-clip MIDI note \"${note.name}\" on track \"${this.id}\":`,\n err\n );\n }\n }\n }\n }\n }\n }\n\n /**\n * Stop all sounding notes and cancel scheduled Part events.\n */\n stopAllSources(): void {\n const now = getContext().rawContext.currentTime;\n try {\n this.synth.releaseAll(now);\n this.kickSynth.releaseAll(now);\n this.cymbalSynth.releaseAll(now);\n this.tomSynth.releaseAll(now);\n // NoiseSynth has no releaseAll — it decays naturally via short envelope\n } catch (err) {\n console.warn(`[waveform-playlist] Error releasing synth on track \"${this.id}\":`, err);\n }\n }\n\n /**\n * No-op for MIDI — MIDI uses note velocity, not gain fades.\n */\n prepareFades(_when: number, _offset: number): void {\n // No-op: MIDI clips don't use fade envelopes\n }\n\n /**\n * No-op for MIDI — no fade automation to cancel.\n */\n cancelFades(): void {\n // No-op: MIDI clips don't use fade envelopes\n }\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error during MIDI track \"${this.id}\" effects cleanup:`,\n err\n );\n }\n }\n\n this.stopAllSources();\n\n // Dispose Parts\n this.scheduledClips.forEach(({ part }, index) => {\n try {\n part.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing Part ${index} on MIDI track \"${this.id}\":`,\n err\n );\n }\n });\n\n // Dispose all synths\n const synthsToDispose = [\n this.synth,\n this.kickSynth,\n this.snareSynth,\n this.cymbalSynth,\n this.tomSynth,\n ];\n for (const s of synthsToDispose) {\n try {\n s?.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing synth on MIDI track \"${this.id}\":`, err);\n }\n }\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing volumeNode on MIDI track \"${this.id}\":`,\n err\n );\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing panNode on MIDI track \"${this.id}\":`, err);\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing muteGain on MIDI track \"${this.id}\":`, err);\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","import { Volume, Gain, Panner, Part, ToneAudioNode, getDestination, getContext } from 'tone';\nimport { Track } from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam } from './fades';\nimport type { TrackEffectsFunction } from './ToneTrack';\nimport type { PlayableTrack, MidiClipInfo } from './MidiToneTrack';\nimport type { SoundFontCache } from './SoundFontCache';\n\nexport interface SoundFontToneTrackOptions {\n clips: MidiClipInfo[];\n track: Track;\n soundFontCache: SoundFontCache;\n /** GM program number (0-127) for melodic instruments */\n programNumber?: number;\n /** Whether this track uses percussion bank (channel 9) */\n isPercussion?: boolean;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n}\n\n/** Per-clip scheduling info */\ninterface ScheduledMidiClip {\n clipInfo: MidiClipInfo;\n part: Part;\n}\n\n/**\n * MIDI track that uses SoundFont samples for playback.\n *\n * Instead of PolySynth synthesis, each note triggers the correct instrument\n * sample from an SF2 file, pitch-shifted via AudioBufferSourceNode.playbackRate.\n *\n * Audio graph per note:\n * AudioBufferSourceNode (native, one-shot, pitch-shifted)\n * → GainNode (native, per-note velocity)\n * → Volume.input (Tone.js, shared per-track)\n * → Panner → muteGain → effects/destination\n */\nexport class SoundFontToneTrack implements PlayableTrack {\n /** Rate-limit missing sample warnings — one per class lifetime */\n private static _missingSampleWarned = false;\n private scheduledClips: ScheduledMidiClip[];\n private activeSources: Set<AudioBufferSourceNode> = new Set();\n private soundFontCache: SoundFontCache;\n private programNumber: number;\n private bankNumber: number;\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n\n constructor(options: SoundFontToneTrackOptions) {\n this.track = options.track;\n this.soundFontCache = options.soundFontCache;\n this.programNumber = options.programNumber ?? 0;\n // Bank 128 for percussion (channel 9), bank 0 for melodic\n this.bankNumber = options.isPercussion ? 128 : 0;\n\n // Create shared track-level Tone.js nodes (same chain as ToneTrack)\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n this.panNode = new Panner(options.track.stereoPan);\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create a Tone.Part for each clip, scheduling notes relative to Transport\n this.scheduledClips = options.clips.map((clipInfo) => {\n // Filter notes within the clip's visible window (after trim offset)\n const visibleNotes = clipInfo.notes.filter((note) => {\n const noteEnd = note.time + note.duration;\n return note.time < clipInfo.offset + clipInfo.duration && noteEnd > clipInfo.offset;\n });\n\n const absClipStart = this.track.startTime + clipInfo.startTime;\n\n const partEvents = visibleNotes.map((note) => {\n const adjustedTime = note.time - clipInfo.offset;\n const clampedStart = Math.max(0, adjustedTime);\n const clampedDuration = Math.min(\n note.duration - Math.max(0, clipInfo.offset - note.time),\n clipInfo.duration - clampedStart\n );\n\n return {\n time: absClipStart + clampedStart,\n note: note.name,\n midi: note.midi,\n duration: Math.max(0, clampedDuration),\n velocity: note.velocity,\n channel: note.channel,\n };\n });\n\n const part = new Part((time, event) => {\n if (event.duration > 0) {\n this.triggerNote(event.midi, event.duration, time, event.velocity, event.channel);\n }\n }, partEvents);\n\n part.start(0);\n\n return { clipInfo, part };\n });\n }\n\n /**\n * Trigger a note by creating a native AudioBufferSourceNode from the SoundFont cache.\n *\n * Per-note routing: channel 9 → bank 128 (drums), others → bank 0 with programNumber.\n */\n private triggerNote(\n midiNote: number,\n duration: number,\n time: number,\n velocity: number,\n channel?: number\n ): void {\n const bank = channel === 9 ? 128 : this.bankNumber;\n const preset = channel === 9 ? 0 : this.programNumber;\n\n const sfSample = this.soundFontCache.getAudioBuffer(midiNote, bank, preset);\n if (!sfSample) {\n if (!SoundFontToneTrack._missingSampleWarned) {\n console.warn(\n `[waveform-playlist] SoundFont sample not found for MIDI note ${midiNote} (bank ${bank}, preset ${preset}). ` +\n 'Subsequent missing samples will be silent.'\n );\n SoundFontToneTrack._missingSampleWarned = true;\n }\n return;\n }\n\n const rawContext = getContext().rawContext as BaseAudioContext;\n\n // Create AudioBufferSourceNode with optional looping\n const source = rawContext.createBufferSource();\n source.buffer = sfSample.buffer;\n source.playbackRate.value = sfSample.playbackRate;\n\n // Enable sample looping if SF2 zone defines loop mode\n if (sfSample.loopMode === 1 || sfSample.loopMode === 3) {\n source.loop = true;\n source.loopStart = sfSample.loopStart;\n source.loopEnd = sfSample.loopEnd;\n }\n\n // For non-looping samples (percussion, one-shots), use the sample's natural\n // buffer duration so cymbals/drums ring out fully. For looping samples\n // (sustained instruments), use the MIDI note duration for note-off timing.\n const sampleDuration = sfSample.buffer.duration / sfSample.playbackRate;\n const effectiveDuration =\n sfSample.loopMode === 0 ? Math.max(duration, sampleDuration) : duration;\n\n // Per-note gain with SF2 volume ADSR envelope\n const peakGain = velocity * velocity;\n const gainNode = rawContext.createGain();\n const { attackVolEnv, holdVolEnv, decayVolEnv, sustainVolEnv, releaseVolEnv } = sfSample;\n const sustainGain = peakGain * sustainVolEnv;\n\n // Attack: start silent, ramp to peak\n gainNode.gain.setValueAtTime(0, time);\n gainNode.gain.linearRampToValueAtTime(peakGain, time + attackVolEnv);\n // Hold at peak\n if (holdVolEnv > 0.001) {\n gainNode.gain.setValueAtTime(peakGain, time + attackVolEnv + holdVolEnv);\n }\n // Decay to sustain level\n const decayStart = time + attackVolEnv + holdVolEnv;\n gainNode.gain.linearRampToValueAtTime(sustainGain, decayStart + decayVolEnv);\n // Sustain holds until note-off, then release ramps to silence.\n // For short notes (duration < AHD), setValueAtTime at note-off cancels the\n // incomplete decay ramp — Web Audio handles this correctly.\n gainNode.gain.setValueAtTime(sustainGain, time + effectiveDuration);\n gainNode.gain.linearRampToValueAtTime(0, time + effectiveDuration + releaseVolEnv);\n\n // Connect: source → gainNode → Volume.input (Tone.js)\n source.connect(gainNode);\n gainNode.connect((this.volumeNode.input as unknown as Gain).input);\n\n // Track active sources for stopAllSources()\n this.activeSources.add(source);\n source.onended = () => {\n this.activeSources.delete(source);\n try {\n gainNode.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] GainNode already disconnected:', err);\n }\n };\n\n source.start(time);\n source.stop(time + effectiveDuration + releaseVolEnv);\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n /**\n * No-op — Tone.Part handles scheduling internally, no ghost tick guard needed.\n */\n setScheduleGuardOffset(_offset: number): void {\n // No-op\n }\n\n /**\n * Start notes that should already be sounding at the current transport offset.\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n for (const note of clipInfo.notes) {\n const adjustedTime = note.time - clipInfo.offset;\n const noteAbsStart = absClipStart + Math.max(0, adjustedTime);\n const noteAbsEnd = noteAbsStart + note.duration;\n\n if (noteAbsStart < transportOffset && noteAbsEnd > transportOffset) {\n const remainingDuration = noteAbsEnd - transportOffset;\n try {\n this.triggerNote(\n note.midi,\n remainingDuration,\n audioContextTime,\n note.velocity,\n note.channel\n );\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start mid-clip SoundFont note on track \"${this.id}\":`,\n err\n );\n }\n }\n }\n }\n }\n }\n\n /**\n * Stop all active AudioBufferSourceNodes.\n */\n stopAllSources(): void {\n for (const source of this.activeSources) {\n try {\n source.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Error stopping AudioBufferSourceNode:', err);\n }\n }\n this.activeSources.clear();\n }\n\n /** No-op for MIDI — MIDI uses note velocity, not gain fades. */\n prepareFades(_when: number, _offset: number): void {}\n\n /** No-op for MIDI — no fade automation to cancel. */\n cancelFades(): void {}\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error during SoundFont track \"${this.id}\" effects cleanup:`,\n err\n );\n }\n }\n\n this.stopAllSources();\n\n // Dispose Parts\n this.scheduledClips.forEach(({ part }, index) => {\n try {\n part.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing Part ${index} on SoundFont track \"${this.id}\":`,\n err\n );\n }\n });\n\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing volumeNode on SoundFont track \"${this.id}\":`,\n err\n );\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing panNode on SoundFont track \"${this.id}\":`,\n err\n );\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing muteGain on SoundFont track \"${this.id}\":`,\n err\n );\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","import { SoundFont2, GeneratorType } from 'soundfont2';\nimport type { Generator, ZoneMap } from 'soundfont2';\n\n/**\n * Result of looking up a MIDI note in the SoundFont.\n * Contains the AudioBuffer, playbackRate, loop points, and volume envelope.\n */\nexport interface SoundFontSample {\n /** Cached AudioBuffer for this sample */\n buffer: AudioBuffer;\n /** Playback rate to pitch-shift from originalPitch to target note */\n playbackRate: number;\n /** Loop mode: 0=no loop, 1=continuous, 3=sustain loop */\n loopMode: number;\n /** Loop start in seconds, relative to AudioBuffer start */\n loopStart: number;\n /** Loop end in seconds, relative to AudioBuffer start */\n loopEnd: number;\n /** Volume envelope attack time in seconds */\n attackVolEnv: number;\n /** Volume envelope hold time in seconds */\n holdVolEnv: number;\n /** Volume envelope decay time in seconds */\n decayVolEnv: number;\n /** Volume envelope sustain level as linear gain 0-1 */\n sustainVolEnv: number;\n /** Volume envelope release time in seconds */\n releaseVolEnv: number;\n}\n\n/**\n * Convert SF2 timecents to seconds.\n * SF2 formula: seconds = 2^(timecents / 1200)\n * Default -12000 timecents ≈ 0.001s (effectively instant).\n */\nexport function timecentsToSeconds(tc: number): number {\n return Math.pow(2, tc / 1200);\n}\n\n/** Max release time to prevent extremely long tails from stale generators */\nconst MAX_RELEASE_SECONDS = 5;\n\n/**\n * Get a numeric generator value from a zone map.\n */\nexport function getGeneratorValue(\n generators: ZoneMap<Generator>,\n type: GeneratorType\n): number | undefined {\n return generators[type]?.value;\n}\n\n/**\n * Convert Int16Array sample data to Float32Array.\n * SF2 samples are 16-bit signed integers; Web Audio needs Float32 [-1, 1].\n */\nexport function int16ToFloat32(samples: Int16Array): Float32Array {\n const floats = new Float32Array(samples.length);\n for (let i = 0; i < samples.length; i++) {\n floats[i] = samples[i] / 32768;\n }\n return floats;\n}\n\n/**\n * Input parameters for playback rate calculation.\n */\nexport interface PlaybackRateParams {\n /** Target MIDI note number (0-127) */\n midiNote: number;\n /** OverridingRootKey generator value, or undefined if not set */\n overrideRootKey: number | undefined;\n /** sample.header.originalPitch (255 means unpitched) */\n originalPitch: number;\n /** CoarseTune generator value in semitones (default 0) */\n coarseTune: number;\n /** FineTune generator value in cents (default 0) */\n fineTune: number;\n /** sample.header.pitchCorrection in cents (default 0) */\n pitchCorrection: number;\n}\n\n/**\n * Calculate playback rate for a MIDI note using the SF2 generator chain.\n *\n * SF2 root key resolution priority:\n * 1. OverridingRootKey generator (per-zone, most specific)\n * 2. sample.header.originalPitch (sample header)\n * 3. MIDI note 60 (middle C fallback)\n *\n * Tuning adjustments:\n * - CoarseTune generator (semitones, additive)\n * - FineTune generator (cents, additive)\n * - sample.header.pitchCorrection (cents, additive)\n */\nexport function calculatePlaybackRate(params: PlaybackRateParams): number {\n const { midiNote, overrideRootKey, originalPitch, coarseTune, fineTune, pitchCorrection } =\n params;\n\n // Resolve root key: OverridingRootKey → originalPitch → 60\n const rootKey =\n overrideRootKey !== undefined ? overrideRootKey : originalPitch !== 255 ? originalPitch : 60;\n\n // Total offset in semitones: target note - root key + tuning\n const totalSemitones = midiNote - rootKey + coarseTune + (fineTune + pitchCorrection) / 100;\n\n return Math.pow(2, totalSemitones / 12);\n}\n\n/**\n * Input parameters for loop and envelope extraction.\n */\nexport interface LoopAndEnvelopeParams {\n /** SF2 generators zone map */\n generators: ZoneMap<Generator>;\n /** Sample header with loop points and sample rate */\n header: {\n startLoop: number;\n endLoop: number;\n sampleRate: number;\n };\n}\n\n/**\n * Extract loop points and volume envelope data from per-zone generators.\n *\n * Loop points are stored as absolute indices into the SF2 sample pool.\n * We convert to AudioBuffer-relative seconds by subtracting header.start\n * and dividing by sampleRate.\n *\n * Volume envelope times are in SF2 timecents; sustain is centibels attenuation.\n */\nexport function extractLoopAndEnvelope(\n params: LoopAndEnvelopeParams\n): Omit<SoundFontSample, 'buffer' | 'playbackRate'> {\n const { generators, header } = params;\n\n // --- Loop points ---\n const loopMode = getGeneratorValue(generators, GeneratorType.SampleModes) ?? 0;\n\n // Compute actual loop positions (header + fine/coarse generator offsets)\n const rawLoopStart =\n header.startLoop +\n (getGeneratorValue(generators, GeneratorType.StartLoopAddrsOffset) ?? 0) +\n (getGeneratorValue(generators, GeneratorType.StartLoopAddrsCoarseOffset) ?? 0) * 32768;\n const rawLoopEnd =\n header.endLoop +\n (getGeneratorValue(generators, GeneratorType.EndLoopAddrsOffset) ?? 0) +\n (getGeneratorValue(generators, GeneratorType.EndLoopAddrsCoarseOffset) ?? 0) * 32768;\n\n // The soundfont2 library already converts startLoop/endLoop to be\n // relative to sample.data (subtracts header.start during parsing),\n // so we only need to divide by sampleRate to get seconds.\n const loopStart = rawLoopStart / header.sampleRate;\n const loopEnd = rawLoopEnd / header.sampleRate;\n\n // --- Volume envelope ---\n const attackVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.AttackVolEnv) ?? -12000\n );\n const holdVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.HoldVolEnv) ?? -12000\n );\n const decayVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.DecayVolEnv) ?? -12000\n );\n const releaseVolEnv = Math.min(\n timecentsToSeconds(getGeneratorValue(generators, GeneratorType.ReleaseVolEnv) ?? -12000),\n MAX_RELEASE_SECONDS\n );\n\n // SustainVolEnv is centibels attenuation: 0 = full volume, 1440 = silence\n // Convert to linear gain: 10^(-cb / 200)\n const sustainCb = getGeneratorValue(generators, GeneratorType.SustainVolEnv) ?? 0;\n const sustainVolEnv = Math.pow(10, -sustainCb / 200);\n\n return {\n loopMode,\n loopStart,\n loopEnd,\n attackVolEnv,\n holdVolEnv,\n decayVolEnv,\n sustainVolEnv,\n releaseVolEnv,\n };\n}\n\n/**\n * Caches parsed SoundFont2 data and AudioBuffers for efficient playback.\n *\n * AudioBuffers are created lazily on first access and cached by sample index.\n * Pitch calculation uses the SF2 generator chain:\n * OverridingRootKey → sample.header.originalPitch → fallback 60\n *\n * Audio graph per note:\n * AudioBufferSourceNode (playbackRate for pitch) → GainNode (velocity) → track chain\n */\nexport class SoundFontCache {\n private sf2: SoundFont2 | null = null;\n private audioBufferCache: Map<number, AudioBuffer> = new Map();\n private context: BaseAudioContext;\n\n /**\n * @param context Optional AudioContext for createBuffer(). If omitted, uses\n * an OfflineAudioContext which doesn't require user gesture — safe to\n * construct before user interaction (avoids Firefox autoplay warnings).\n */\n constructor(context?: BaseAudioContext) {\n // OfflineAudioContext only needs valid params; we never call startRendering().\n // It's used solely for createBuffer() which works identically to AudioContext.\n this.context = context ?? new OfflineAudioContext(1, 1, 44100);\n }\n\n /**\n * Load and parse an SF2 file from a URL.\n */\n async load(url: string, signal?: AbortSignal): Promise<void> {\n const response = await fetch(url, { signal });\n if (!response.ok) {\n throw new Error(`Failed to fetch SoundFont ${url}: ${response.statusText}`);\n }\n const arrayBuffer = await response.arrayBuffer();\n try {\n this.sf2 = new SoundFont2(new Uint8Array(arrayBuffer));\n } catch (err) {\n throw new Error(\n `Failed to parse SoundFont ${url}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n /**\n * Load from an already-fetched ArrayBuffer.\n */\n loadFromBuffer(data: ArrayBuffer): void {\n try {\n this.sf2 = new SoundFont2(new Uint8Array(data));\n } catch (err) {\n throw new Error(\n `Failed to parse SoundFont from buffer: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n get isLoaded(): boolean {\n return this.sf2 !== null;\n }\n\n /**\n * Look up a MIDI note and return the AudioBuffer + playbackRate.\n *\n * @param midiNote - MIDI note number (0-127)\n * @param bankNumber - Bank number (0 for melodic, 128 for percussion/drums)\n * @param presetNumber - GM program number (0-127)\n * @returns SoundFontSample or null if no sample found for this note\n */\n getAudioBuffer(\n midiNote: number,\n bankNumber: number = 0,\n presetNumber: number = 0\n ): SoundFontSample | null {\n if (!this.sf2) return null;\n\n const keyData = this.sf2.getKeyData(midiNote, bankNumber, presetNumber);\n if (!keyData) return null;\n\n const sample = keyData.sample;\n const sampleIndex = this.sf2.samples.indexOf(sample);\n\n // Get or create the AudioBuffer for this sample\n let buffer = this.audioBufferCache.get(sampleIndex);\n if (!buffer) {\n buffer = this.int16ToAudioBuffer(sample.data, sample.header.sampleRate);\n this.audioBufferCache.set(sampleIndex, buffer);\n }\n\n // Calculate playback rate using SF2 generator chain for root key.\n // Priority: OverridingRootKey generator → sample.header.originalPitch → 60\n const playbackRate = calculatePlaybackRate({\n midiNote,\n overrideRootKey: getGeneratorValue(keyData.generators, GeneratorType.OverridingRootKey),\n originalPitch: sample.header.originalPitch,\n coarseTune: getGeneratorValue(keyData.generators, GeneratorType.CoarseTune) ?? 0,\n fineTune: getGeneratorValue(keyData.generators, GeneratorType.FineTune) ?? 0,\n pitchCorrection: sample.header.pitchCorrection ?? 0,\n });\n\n // Extract per-zone loop points and volume envelope from generators\n const loopAndEnvelope = extractLoopAndEnvelope({\n generators: keyData.generators,\n header: keyData.sample.header,\n });\n\n return { buffer, playbackRate, ...loopAndEnvelope };\n }\n\n /**\n * Convert Int16Array sample data to an AudioBuffer.\n * Uses the extracted int16ToFloat32 for the conversion, then copies into an AudioBuffer.\n */\n private int16ToAudioBuffer(data: Int16Array, sampleRate: number): AudioBuffer {\n const floats = int16ToFloat32(data);\n const buffer = this.context.createBuffer(1, floats.length, sampleRate);\n buffer.getChannelData(0).set(floats);\n return buffer;\n }\n\n /**\n * Clear all cached AudioBuffers and release the parsed SF2.\n */\n dispose(): void {\n this.audioBufferCache.clear();\n this.sf2 = null;\n }\n}\n","/**\n * Global AudioContext Manager\n *\n * Provides a single AudioContext shared across the entire application.\n * This context is used by Tone.js for playback and by all recording/monitoring hooks.\n *\n * Uses Tone.js's Context class which wraps standardized-audio-context for\n * cross-browser compatibility (fixes Firefox AudioListener issues).\n */\n\nimport { Context, setContext } from 'tone';\n\nlet globalToneContext: Context | null = null;\n\n/**\n * Get the global Tone.js Context\n * This is the main context for cross-browser audio operations.\n * Use context.createAudioWorkletNode(), context.createMediaStreamSource(), etc.\n * @returns The Tone.js Context instance\n */\nexport function getGlobalContext(): Context {\n if (!globalToneContext) {\n globalToneContext = new Context();\n setContext(globalToneContext);\n }\n return globalToneContext;\n}\n\n/**\n * Get or create the global AudioContext\n * Uses Tone.js Context for cross-browser compatibility\n * @returns The global AudioContext instance (rawContext from Tone.Context)\n */\nexport function getGlobalAudioContext(): AudioContext {\n return getGlobalContext().rawContext as AudioContext;\n}\n\n/**\n * @deprecated Use getGlobalContext() instead\n * Get the Tone.js Context's rawContext typed as IAudioContext\n * @returns The rawContext cast as IAudioContext\n */\nexport function getGlobalToneContext(): Context {\n return getGlobalContext();\n}\n\n/**\n * Resume the global AudioContext if it's suspended\n * Should be called in response to a user gesture (e.g., button click)\n * @returns Promise that resolves when context is running\n */\nexport async function resumeGlobalAudioContext(): Promise<void> {\n const context = getGlobalContext();\n if (context.state !== 'running') {\n await context.resume();\n }\n}\n\n/**\n * Get the current state of the global AudioContext\n * @returns The AudioContext state ('suspended', 'running', or 'closed')\n */\nexport function getGlobalAudioContextState(): AudioContextState {\n return globalToneContext?.rawContext.state || 'suspended';\n}\n\n/**\n * Close the global AudioContext\n * Should only be called when the application is shutting down\n */\nexport async function closeGlobalAudioContext(): Promise<void> {\n if (globalToneContext && globalToneContext.rawContext.state !== 'closed') {\n await globalToneContext.close();\n globalToneContext = null;\n }\n}\n","/**\n * MediaStreamSource Manager\n *\n * Manages MediaStreamAudioSourceNode instances to ensure only one source\n * is created per MediaStream per AudioContext.\n *\n * Web Audio API constraint: You can only create one MediaStreamAudioSourceNode\n * per MediaStream per AudioContext. Multiple attempts will fail or disconnect\n * previous sources.\n *\n * This manager ensures a single source is shared across multiple consumers\n * (e.g., AnalyserNode for VU meter, AudioWorkletNode for recording).\n *\n * NOTE: With Tone.js Context, you can also use context.createMediaStreamSource()\n * directly, which handles cross-browser compatibility internally.\n */\n\nimport { getContext } from 'tone';\n\n// Map of MediaStream -> MediaStreamAudioSourceNode\nconst streamSources = new Map<MediaStream, MediaStreamAudioSourceNode>();\n\n// Map of MediaStream -> cleanup handlers\nconst streamCleanupHandlers = new Map<MediaStream, () => void>();\n\n/**\n * Get or create a MediaStreamAudioSourceNode for the given stream\n *\n * @param stream - The MediaStream to create a source for\n * @returns MediaStreamAudioSourceNode that can be connected to multiple nodes\n *\n * @example\n * ```typescript\n * const source = getMediaStreamSource(stream);\n *\n * // Multiple consumers can connect to the same source\n * source.connect(analyserNode); // For VU meter\n * source.connect(workletNode); // For recording\n * ```\n */\nexport function getMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode {\n // Return existing source if we have one for this stream\n if (streamSources.has(stream)) {\n return streamSources.get(stream)!;\n }\n\n // Create new source using Tone.js's shared context for cross-browser compatibility\n const context = getContext();\n const source = context.createMediaStreamSource(stream);\n streamSources.set(stream, source);\n\n // Set up cleanup when stream ends\n const cleanup = () => {\n source.disconnect();\n streamSources.delete(stream);\n streamCleanupHandlers.delete(stream);\n\n // Remove event listener\n stream.removeEventListener('ended', cleanup);\n stream.removeEventListener('inactive', cleanup);\n };\n\n streamCleanupHandlers.set(stream, cleanup);\n\n // Clean up when stream ends or becomes inactive\n stream.addEventListener('ended', cleanup);\n stream.addEventListener('inactive', cleanup);\n\n return source;\n}\n\n/**\n * Manually release a MediaStreamSource\n *\n * Normally you don't need to call this - cleanup happens automatically\n * when the stream ends. Only call this if you need to force cleanup.\n *\n * @param stream - The MediaStream to release the source for\n */\nexport function releaseMediaStreamSource(stream: MediaStream): void {\n const cleanup = streamCleanupHandlers.get(stream);\n if (cleanup) {\n cleanup();\n }\n}\n\n/**\n * Check if a MediaStreamSource exists for the given stream\n *\n * @param stream - The MediaStream to check\n * @returns true if a source exists for this stream\n */\nexport function hasMediaStreamSource(stream: MediaStream): boolean {\n return streamSources.has(stream);\n}\n","import type { ClipTrack, Track } from '@waveform-playlist/core';\nimport {\n clipStartTime,\n clipEndTime,\n clipOffsetTime,\n clipDurationTime,\n} from '@waveform-playlist/core';\nimport type { PlayoutAdapter } from '@waveform-playlist/engine';\nimport { TonePlayout } from './TonePlayout';\nimport type { EffectsFunction } from './TonePlayout';\nimport type { ClipInfo } from './ToneTrack';\nimport type { MidiClipInfo } from './MidiToneTrack';\nimport type { SoundFontCache } from './SoundFontCache';\nimport { now } from 'tone';\n\nexport interface ToneAdapterOptions {\n effects?: EffectsFunction;\n /** When provided, MIDI clips use SoundFont sample playback instead of PolySynth */\n soundFontCache?: SoundFontCache;\n}\n\nexport function createToneAdapter(options?: ToneAdapterOptions): PlayoutAdapter {\n let playout: TonePlayout | null = null;\n let _isPlaying = false;\n let _playoutGeneration = 0;\n let _loopEnabled = false;\n let _loopStart = 0;\n let _loopEnd = 0;\n let _audioInitialized = false;\n\n // Add a single ClipTrack to the playout (shared by buildPlayout and addTrack)\n function addTrackToPlayout(p: TonePlayout, track: ClipTrack): void {\n const audioClips = track.clips.filter((c) => c.audioBuffer && !c.midiNotes);\n const midiClips = track.clips.filter((c) => c.midiNotes && c.midiNotes.length > 0);\n\n if (audioClips.length > 0) {\n const startTime = Math.min(...audioClips.map(clipStartTime));\n const endTime = Math.max(...audioClips.map(clipEndTime));\n\n const trackObj: Track = {\n id: track.id,\n name: track.name,\n gain: track.volume,\n muted: track.muted,\n soloed: track.soloed,\n stereoPan: track.pan,\n startTime,\n endTime,\n };\n\n const clipInfos: ClipInfo[] = audioClips.map((clip) => ({\n buffer: clip.audioBuffer!,\n startTime: clipStartTime(clip) - startTime,\n duration: clipDurationTime(clip),\n offset: clipOffsetTime(clip),\n fadeIn: clip.fadeIn,\n fadeOut: clip.fadeOut,\n gain: clip.gain,\n }));\n\n p.addTrack({\n clips: clipInfos,\n track: trackObj,\n effects: track.effects,\n });\n }\n\n if (midiClips.length > 0) {\n const startTime = Math.min(...midiClips.map(clipStartTime));\n const endTime = Math.max(...midiClips.map(clipEndTime));\n\n const trackId = audioClips.length > 0 ? `${track.id}:midi` : track.id;\n\n const trackObj: Track = {\n id: trackId,\n name: track.name,\n gain: track.volume,\n muted: track.muted,\n soloed: track.soloed,\n stereoPan: track.pan,\n startTime,\n endTime,\n };\n\n const midiClipInfos: MidiClipInfo[] = midiClips.map((clip) => ({\n notes: clip.midiNotes!,\n startTime: clipStartTime(clip) - startTime,\n duration: clipDurationTime(clip),\n offset: clipOffsetTime(clip),\n }));\n\n if (options?.soundFontCache?.isLoaded) {\n const firstClip = midiClips[0];\n const midiChannel = firstClip.midiChannel;\n const isPercussion = midiChannel === 9;\n const programNumber = firstClip.midiProgram ?? 0;\n\n p.addSoundFontTrack({\n clips: midiClipInfos,\n track: trackObj,\n soundFontCache: options.soundFontCache,\n programNumber,\n isPercussion,\n effects: track.effects,\n });\n } else {\n if (options?.soundFontCache) {\n console.warn(\n `[waveform-playlist] SoundFont not loaded for track \"${track.name}\" — falling back to PolySynth.`\n );\n }\n p.addMidiTrack({\n clips: midiClipInfos,\n track: trackObj,\n effects: track.effects,\n });\n }\n }\n }\n\n function buildPlayout(tracks: ClipTrack[]): void {\n if (playout) {\n try {\n playout.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing previous playout during rebuild:', err);\n }\n playout = null;\n }\n\n _playoutGeneration++;\n const generation = _playoutGeneration;\n\n playout = new TonePlayout({\n effects: options?.effects,\n });\n\n // If Tone.start() was already called (AudioContext resumed), carry\n // initialization forward. Tone.start() is safe to call multiple times —\n // it resolves immediately if the AudioContext is already running.\n if (_audioInitialized) {\n playout.init().catch((err) => {\n console.warn(\n '[waveform-playlist] Failed to re-initialize playout after rebuild. ' +\n 'Audio playback will require another user gesture.',\n err\n );\n _audioInitialized = false;\n });\n }\n\n for (const track of tracks) {\n addTrackToPlayout(playout, track);\n }\n playout.applyInitialSoloState();\n playout.setLoop(_loopEnabled, _loopStart, _loopEnd);\n\n playout.setOnPlaybackComplete(() => {\n if (generation === _playoutGeneration) {\n _isPlaying = false;\n }\n });\n }\n\n return {\n async init(): Promise<void> {\n if (playout) {\n await playout.init();\n _audioInitialized = true;\n }\n },\n\n setTracks(tracks: ClipTrack[]): void {\n buildPlayout(tracks);\n },\n\n addTrack(track: ClipTrack): void {\n if (!playout) {\n throw new Error(\n '[waveform-playlist] adapter.addTrack() called but no playout exists. ' +\n 'Call setTracks() first to initialize the playout.'\n );\n }\n addTrackToPlayout(playout, track);\n playout.applyInitialSoloState();\n },\n\n play(startTime: number, endTime?: number): void {\n if (!playout) {\n console.warn(\n '[waveform-playlist] adapter.play() called but no playout is available. ' +\n 'Tracks may not have been set, or the adapter was disposed.'\n );\n return;\n }\n const duration = endTime !== undefined ? endTime - startTime : undefined;\n playout.play(now(), startTime, duration);\n // Only set _isPlaying if play() didn't throw\n // (TonePlayout.play() re-throws after cleanup on Transport failure)\n _isPlaying = true;\n },\n\n pause(): void {\n playout?.pause();\n _isPlaying = false;\n },\n\n stop(): void {\n playout?.stop();\n _isPlaying = false;\n },\n\n seek(time: number): void {\n playout?.seekTo(time);\n },\n\n getCurrentTime(): number {\n return playout?.getCurrentTime() ?? 0;\n },\n\n isPlaying(): boolean {\n return _isPlaying;\n },\n\n setMasterVolume(volume: number): void {\n playout?.setMasterGain(volume);\n },\n\n setTrackVolume(trackId: string, volume: number): void {\n playout?.getTrack(trackId)?.setVolume(volume);\n },\n\n setTrackMute(trackId: string, muted: boolean): void {\n playout?.setMute(trackId, muted);\n },\n\n setTrackSolo(trackId: string, soloed: boolean): void {\n playout?.setSolo(trackId, soloed);\n },\n\n setTrackPan(trackId: string, pan: number): void {\n playout?.getTrack(trackId)?.setPan(pan);\n },\n\n setLoop(enabled: boolean, start: number, end: number): void {\n _loopEnabled = enabled;\n _loopStart = start;\n _loopEnd = end;\n playout?.setLoop(enabled, start, end);\n },\n\n dispose(): void {\n try {\n playout?.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing playout:', err);\n }\n playout = null;\n _isPlaying = false;\n },\n };\n}\n"],"mappings":"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","names":["import_tone","clipStartTime","import_tone","now","import_tone","import_tone","import_tone","import_core","import_tone","start"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/TonePlayout.ts","../src/ToneTrack.ts","../src/fades.ts","../src/MidiToneTrack.ts","../src/SoundFontToneTrack.ts","../src/SoundFontCache.ts","../src/audioContext.ts","../src/mediaStreamSourceManager.ts","../src/TonePlayoutAdapter.ts"],"sourcesContent":["export { TonePlayout } from './TonePlayout';\nexport { ToneTrack } from './ToneTrack';\nexport { MidiToneTrack } from './MidiToneTrack';\nexport type { PlayableTrack, MidiClipInfo, MidiToneTrackOptions } from './MidiToneTrack';\nexport { SoundFontToneTrack } from './SoundFontToneTrack';\nexport type { SoundFontToneTrackOptions } from './SoundFontToneTrack';\nexport {\n SoundFontCache,\n timecentsToSeconds,\n getGeneratorValue,\n int16ToFloat32,\n calculatePlaybackRate,\n extractLoopAndEnvelope,\n} from './SoundFontCache';\nexport type { SoundFontSample, PlaybackRateParams, LoopAndEnvelopeParams } from './SoundFontCache';\nexport type { TonePlayoutOptions, EffectsFunction } from './TonePlayout';\nexport type { ToneTrackOptions, TrackEffectsFunction } from './ToneTrack';\n\n// Export global AudioContext manager\nexport {\n getGlobalContext,\n getGlobalAudioContext,\n getGlobalToneContext,\n resumeGlobalAudioContext,\n getGlobalAudioContextState,\n closeGlobalAudioContext,\n} from './audioContext';\n\n// Export MediaStreamSource manager\nexport {\n getMediaStreamSource,\n releaseMediaStreamSource,\n hasMediaStreamSource,\n} from './mediaStreamSourceManager';\n\n// Export fade utilities (pure functions re-exported from @waveform-playlist/core)\nexport { applyFadeIn, applyFadeOut, type FadeConfig, type FadeType } from './fades';\n\n// Export Tone.js-specific fade helper\nexport { getUnderlyingAudioParam } from './fades';\n\n// Export Tone.js adapter for engine integration\nexport { createToneAdapter } from './TonePlayoutAdapter';\nexport type { ToneAdapterOptions } from './TonePlayoutAdapter';\n","import {\n Volume,\n ToneAudioNode,\n getDestination,\n start,\n now,\n getTransport,\n getContext,\n BaseContext,\n} from 'tone';\nimport { ToneTrack, ToneTrackOptions } from './ToneTrack';\nimport { MidiToneTrack, MidiToneTrackOptions } from './MidiToneTrack';\nimport type { PlayableTrack } from './MidiToneTrack';\nimport { SoundFontToneTrack, SoundFontToneTrackOptions } from './SoundFontToneTrack';\n\nexport type EffectsFunction = (\n masterGainNode: Volume,\n destination: ToneAudioNode,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface TonePlayoutOptions {\n tracks?: ToneTrack[];\n masterGain?: number;\n effects?: EffectsFunction;\n}\n\nexport class TonePlayout {\n private tracks: Map<string, PlayableTrack> = new Map();\n private masterVolume: Volume;\n private isInitialized = false;\n private soloedTracks: Set<string> = new Set();\n private manualMuteState: Map<string, boolean> = new Map();\n private effectsCleanup?: () => void;\n private onPlaybackCompleteCallback?: () => void;\n private _completionEventId: number | null = null;\n private _loopHandler: (() => void) | null = null;\n private _loopEnabled = false;\n private _loopStart = 0;\n private _loopEnd = 0;\n\n constructor(options: TonePlayoutOptions = {}) {\n this.masterVolume = new Volume(this.gainToDb(options.masterGain ?? 1));\n\n // Setup effects chain if provided, otherwise connect directly to destination\n if (options.effects) {\n const cleanup = options.effects(this.masterVolume, getDestination(), false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.masterVolume.toDestination();\n }\n\n if (options.tracks) {\n options.tracks.forEach((track) => {\n this.tracks.set(track.id, track);\n this.manualMuteState.set(track.id, track.muted);\n });\n }\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n private clearCompletionEvent(): void {\n if (this._completionEventId !== null) {\n try {\n getTransport().clear(this._completionEventId);\n } catch (err) {\n console.warn('[waveform-playlist] Error clearing Transport completion event:', err);\n }\n this._completionEventId = null;\n }\n }\n\n async init(): Promise<void> {\n if (this.isInitialized) return;\n\n await start();\n this.isInitialized = true;\n }\n\n addTrack(trackOptions: ToneTrackOptions): ToneTrack {\n // Ensure tracks connect to master volume instead of destination\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const toneTrack = new ToneTrack(optionsWithDestination);\n this.tracks.set(toneTrack.id, toneTrack);\n this.manualMuteState.set(toneTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(toneTrack.id);\n }\n return toneTrack;\n }\n\n addMidiTrack(trackOptions: MidiToneTrackOptions): MidiToneTrack {\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const midiTrack = new MidiToneTrack(optionsWithDestination);\n this.tracks.set(midiTrack.id, midiTrack);\n this.manualMuteState.set(midiTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(midiTrack.id);\n }\n return midiTrack;\n }\n\n addSoundFontTrack(trackOptions: SoundFontToneTrackOptions): SoundFontToneTrack {\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const sfTrack = new SoundFontToneTrack(optionsWithDestination);\n this.tracks.set(sfTrack.id, sfTrack);\n this.manualMuteState.set(sfTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(sfTrack.id);\n }\n return sfTrack;\n }\n\n /**\n * Apply solo muting after all tracks have been added.\n * Call this after adding all tracks to ensure solo logic is applied correctly.\n */\n applyInitialSoloState(): void {\n this.updateSoloMuting();\n }\n\n removeTrack(trackId: string): void {\n const track = this.tracks.get(trackId);\n if (track) {\n track.dispose();\n this.tracks.delete(trackId);\n this.manualMuteState.delete(trackId);\n this.soloedTracks.delete(trackId);\n }\n }\n\n getTrack(trackId: string): PlayableTrack | undefined {\n return this.tracks.get(trackId);\n }\n\n play(when?: number, offset?: number, duration?: number): void {\n if (!this.isInitialized) {\n throw new Error('[waveform-playlist] TonePlayout not initialized. Call init() first.');\n }\n\n const startTime = when ?? now();\n const transport = getTransport();\n\n this.clearCompletionEvent();\n\n const transportOffset = offset ?? 0;\n this.tracks.forEach((track) => {\n track.cancelFades();\n track.prepareFades(startTime, transportOffset);\n });\n\n // Schedule duration-limited stop via Transport\n if (duration !== undefined) {\n this._completionEventId = transport.scheduleOnce(() => {\n this._completionEventId = null;\n try {\n this.onPlaybackCompleteCallback?.();\n } catch (err) {\n console.warn('[waveform-playlist] Error in playback completion callback:', err);\n }\n }, transportOffset + duration);\n }\n\n // Start Transport — triggers schedule() callbacks for clips at/after offset\n try {\n // Stop all active native sources before restarting to prevent layered audio.\n // Native AudioBufferSourceNodes don't respond to Transport state changes.\n if (transport.state !== 'stopped') {\n transport.stop();\n }\n this.tracks.forEach((track) => track.stopAllSources());\n\n // Set loop boundaries BEFORE enabling loop. _processTick checks\n // `ticks >= _loopEnd` every tick; _loopEnd defaults to 0.\n transport.loopStart = this._loopStart;\n transport.loopEnd = this._loopEnd;\n transport.loop = this._loopEnabled;\n\n // Set schedule guard BEFORE transport.start(). Ghost ticks from stale\n // Clock._lastUpdate can fire schedule callbacks at past positions;\n // the guard suppresses callbacks for clips before the play offset\n // (those are handled by startMidClipSources below).\n this.tracks.forEach((track) => track.setScheduleGuardOffset(transportOffset));\n\n if (offset !== undefined) {\n transport.start(startTime, offset);\n } else {\n transport.start(startTime);\n }\n\n // Advance Clock._lastUpdate past the stop/start boundary.\n //\n // After stop/start cycles, _lastUpdate is stale (set by the previous\n // context tick, before our stop/start). The next Clock._loop() processes\n // ticks from [_lastUpdate, now()]. In the gap [_lastUpdate, startTime),\n // the TickSource is still \"started\" from the PREVIOUS play cycle with\n // its old tick offset. Those accumulated ticks can exceed _loopEnd,\n // causing _processTick to wrap immediately to loopStart.\n //\n // By advancing _lastUpdate to startTime, we skip the stale range.\n // The next Clock._loop() only processes [startTime, now()] — ticks\n // from the current play cycle with the correct offset.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tone.js private internal, see CLAUDE.md ghost tick fix\n (transport as any)._clock._lastUpdate = startTime;\n\n // Start sources for clips that span the current Transport position.\n // Transport.schedule() only fires for clips at/after the offset;\n // clips whose start time is before the offset need manual creation.\n this.tracks.forEach((track) => {\n track.startMidClipSources(transportOffset, startTime);\n });\n } catch (err) {\n // Clean up scheduled events since Transport failed to start\n this.clearCompletionEvent();\n this.tracks.forEach((track) => track.cancelFades());\n console.warn(\n '[waveform-playlist] Transport.start() failed. Audio playback could not begin.',\n err\n );\n throw err;\n }\n }\n\n pause(): void {\n const transport = getTransport();\n try {\n transport.pause();\n } catch (err) {\n console.warn('[waveform-playlist] Transport.pause() failed:', err);\n }\n // Native AudioBufferSourceNodes ignore Transport state changes —\n // they must be explicitly stopped.\n this.tracks.forEach((track) => track.stopAllSources());\n this.tracks.forEach((track) => track.cancelFades());\n this.clearCompletionEvent();\n }\n\n stop(): void {\n const transport = getTransport();\n try {\n transport.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Transport.stop() failed:', err);\n }\n // Remove loop handler before stopping sources. Prevents any deferred\n // loop event from creating new sources via startMidClipSources() after\n // stopAllSources() runs. Defense-in-depth — setLoop(false) should\n // already have removed it, but stop() must be self-contained.\n if (this._loopHandler) {\n try {\n transport.off('loop', this._loopHandler);\n } catch (err) {\n console.warn('[waveform-playlist] Error removing loop handler:', err);\n }\n this._loopHandler = null;\n }\n // Stop all native sources explicitly\n this.tracks.forEach((track) => {\n try {\n track.stopAllSources();\n } catch (err) {\n console.warn(`[waveform-playlist] Error stopping sources for track \"${track.id}\":`, err);\n }\n });\n this.tracks.forEach((track) => {\n try {\n track.cancelFades();\n } catch (err) {\n console.warn(`[waveform-playlist] Error canceling fades for track \"${track.id}\":`, err);\n }\n });\n this.clearCompletionEvent();\n }\n\n setMasterGain(gain: number): void {\n this.masterVolume.volume.value = this.gainToDb(gain);\n }\n\n setSolo(trackId: string, soloed: boolean): void {\n const track = this.tracks.get(trackId);\n if (track) {\n track.setSolo(soloed);\n if (soloed) {\n this.soloedTracks.add(trackId);\n } else {\n this.soloedTracks.delete(trackId);\n }\n this.updateSoloMuting();\n }\n }\n\n private updateSoloMuting(): void {\n const hasSoloedTracks = this.soloedTracks.size > 0;\n\n this.tracks.forEach((track, id) => {\n if (hasSoloedTracks) {\n if (!this.soloedTracks.has(id)) {\n track.setMute(true);\n } else {\n const manuallyMuted = this.manualMuteState.get(id) ?? false;\n track.setMute(manuallyMuted);\n }\n } else {\n const manuallyMuted = this.manualMuteState.get(id) ?? false;\n track.setMute(manuallyMuted);\n }\n });\n }\n\n setMute(trackId: string, muted: boolean): void {\n const track = this.tracks.get(trackId);\n if (track) {\n this.manualMuteState.set(trackId, muted);\n track.setMute(muted);\n }\n }\n\n setLoop(enabled: boolean, loopStart: number, loopEnd: number): void {\n // Update cached state first — play() uses these values to configure\n // the Transport on next start, so they must reflect the caller's intent\n // regardless of whether Transport property setting succeeds.\n this._loopEnabled = enabled;\n this._loopStart = loopStart;\n this._loopEnd = loopEnd;\n\n const transport = getTransport();\n try {\n // Set boundaries BEFORE enabling loop. Tone.js's _processTick checks\n // `ticks >= _loopEnd` on every tick. If we set transport.loop = true\n // first, a tick could fire before loopEnd is updated, seeing the stale\n // _loopEnd value (0 from Transport default) and wrapping immediately.\n transport.loopStart = loopStart;\n transport.loopEnd = loopEnd;\n transport.loop = enabled;\n } catch (err) {\n console.warn('[waveform-playlist] Error configuring Transport loop:', err);\n return;\n }\n\n if (enabled && !this._loopHandler) {\n this._loopHandler = () => {\n // On loop boundary: stop old sources, re-schedule fades, start mid-clip sources.\n // Event ordering in Transport's tick processing (Tone.js 15.x _processTick):\n // loopEnd → ticks reset → loopStart → loop → forEachAtTime(ticks)\n // Our loop handler fires BEFORE schedule callbacks, so:\n // 1. stopAllSources + cancelFades — clean slate\n // 2. startMidClipSources — for clips spanning loopStart boundary\n // 3. prepareFades — fresh fade envelopes\n // Then Transport fires schedule callbacks for clips at/after loopStart.\n const currentTime = now();\n this.tracks.forEach((track) => {\n try {\n track.stopAllSources();\n track.cancelFades();\n track.setScheduleGuardOffset(this._loopStart);\n track.startMidClipSources(this._loopStart, currentTime);\n track.prepareFades(currentTime, this._loopStart);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error re-scheduling track \"${track.id}\" on loop:`,\n err\n );\n }\n });\n };\n transport.on('loop', this._loopHandler);\n } else if (!enabled && this._loopHandler) {\n transport.off('loop', this._loopHandler);\n this._loopHandler = null;\n }\n }\n\n getCurrentTime(): number {\n return getTransport().seconds;\n }\n\n seekTo(time: number): void {\n getTransport().seconds = time;\n }\n\n dispose(): void {\n // Stop Transport and all active sources before disposing.\n // Without this, the global Transport singleton keeps firing scheduled\n // callbacks and native AudioBufferSourceNodes continue playing through\n // the shared AudioContext (e.g., during Docusaurus client-side navigation).\n try {\n this.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Error stopping Transport during dispose:', err);\n }\n\n this.tracks.forEach((track) => {\n try {\n track.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing track \"${track.id}\":`, err);\n }\n });\n this.tracks.clear();\n\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn('[waveform-playlist] Error during master effects cleanup:', err);\n }\n }\n\n try {\n this.masterVolume.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing master volume:', err);\n }\n }\n\n get context(): BaseContext {\n return getContext();\n }\n\n get sampleRate(): number {\n return getContext().sampleRate;\n }\n\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n}\n","import {\n Volume,\n Gain,\n Panner,\n ToneAudioNode,\n getDestination,\n getTransport,\n getContext,\n} from 'tone';\nimport { Track, type Fade } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, getUnderlyingAudioParam } from './fades';\n\nexport type TrackEffectsFunction = (\n graphEnd: Gain,\n masterGainNode: ToneAudioNode,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface ClipInfo {\n buffer: AudioBuffer;\n startTime: number; // When this clip starts in the track timeline (seconds)\n duration: number; // How long this clip plays (seconds)\n offset: number; // Where to start playing within the buffer (seconds)\n fadeIn?: Fade;\n fadeOut?: Fade;\n gain: number; // Clip-level gain\n}\n\nexport interface ToneTrackOptions {\n buffer?: AudioBuffer; // Legacy: single buffer (deprecated, use clips instead)\n clips?: ClipInfo[]; // Modern: array of clips\n track: Track;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n}\n\n/** Per-clip scheduling info and audio nodes */\ninterface ScheduledClip {\n clipInfo: ClipInfo;\n fadeGainNode: GainNode; // Native GainNode for per-clip fade envelope\n scheduleId: number; // Transport.schedule() event ID\n}\n\nexport class ToneTrack {\n private scheduledClips: ScheduledClip[];\n private activeSources: Set<AudioBufferSourceNode> = new Set();\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n // Guard against ghost tick schedule callbacks. After stop/start cycles with\n // loops, stale Clock._lastUpdate causes ticks from the previous cycle to fire\n // Transport.schedule() callbacks at past positions (e.g., time 0 clips fire\n // when starting at offset 5s). Clips before this offset are handled by\n // startMidClipSources(); schedule callbacks should only create sources for\n // clips at/after this offset.\n private _scheduleGuardOffset = 0;\n\n constructor(options: ToneTrackOptions) {\n this.track = options.track;\n\n // Create shared track-level Tone.js nodes\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n // Tone.js Panner defaults to channelCount: 1 + channelCountMode: 'explicit',\n // which forces stereo→mono downmix (1/√2 attenuation) before panning.\n // Override to channelCount: 2 to preserve stereo recordings.\n this.panNode = new Panner({ pan: options.track.stereoPan, channelCount: 2 });\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n\n // Chain shared Tone.js nodes: Volume → Pan → MuteGain\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create clips array - support both legacy single buffer and modern clips array\n const clipInfos: ClipInfo[] =\n options.clips ||\n (options.buffer\n ? [\n {\n buffer: options.buffer,\n startTime: 0,\n duration: options.buffer.duration,\n offset: 0,\n fadeIn: options.track.fadeIn,\n fadeOut: options.track.fadeOut,\n gain: 1,\n },\n ]\n : []);\n\n const transport = getTransport();\n const rawContext = getContext().rawContext as AudioContext;\n\n // Get the native AudioNode input of the Volume for native→Tone connection.\n // Volume.input is a Tone.js Gain<\"decibels\"> whose .input is the native GainNode.\n // Cast through unknown since Gain<\"decibels\"> and Gain<\"gain\"> don't overlap.\n const volumeNativeInput = (this.volumeNode.input as unknown as Gain).input;\n\n // Schedule each clip via Transport.schedule() with native AudioBufferSourceNode\n this.scheduledClips = clipInfos.map((clipInfo) => {\n // Native GainNode for per-clip fade envelope — created once, reused across play cycles\n const fadeGainNode = rawContext.createGain();\n fadeGainNode.gain.value = clipInfo.gain;\n fadeGainNode.connect(volumeNativeInput);\n\n // Schedule a permanent Transport event at the clip's absolute timeline position.\n // This callback fires on every play and every loop iteration when Transport\n // passes this point.\n const absTransportTime = this.track.startTime + clipInfo.startTime;\n const scheduleId = transport.schedule((audioContextTime: number) => {\n // Guard: ghost ticks from stale Clock._lastUpdate can fire this callback\n // at past positions (see Tone.js #1419). Clips before the play/loop offset\n // are already handled by startMidClipSources().\n if (absTransportTime < this._scheduleGuardOffset) {\n return;\n }\n this.startClipSource(clipInfo, fadeGainNode, audioContextTime);\n }, absTransportTime);\n\n return { clipInfo, fadeGainNode, scheduleId };\n });\n }\n\n /**\n * Create and start an AudioBufferSourceNode for a clip.\n * Sources are one-shot: each play or loop iteration creates a fresh one.\n */\n private startClipSource(\n clipInfo: ClipInfo,\n fadeGainNode: GainNode,\n audioContextTime: number,\n bufferOffset?: number,\n playDuration?: number\n ): void {\n const rawContext = getContext().rawContext as AudioContext;\n const source = rawContext.createBufferSource();\n source.buffer = clipInfo.buffer;\n source.connect(fadeGainNode);\n\n const offset = bufferOffset ?? clipInfo.offset;\n const duration = playDuration ?? clipInfo.duration;\n\n try {\n source.start(audioContextTime, offset, duration);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start source on track \"${this.id}\" ` +\n `(time=${audioContextTime}, offset=${offset}, duration=${duration}):`,\n err\n );\n source.disconnect();\n return;\n }\n\n this.activeSources.add(source);\n source.onended = () => {\n this.activeSources.delete(source);\n };\n }\n\n /**\n * Set the schedule guard offset. Schedule callbacks for clips before this\n * offset are suppressed (already handled by startMidClipSources).\n * Must be called before transport.start() and in the loop handler.\n */\n setScheduleGuardOffset(offset: number): void {\n this._scheduleGuardOffset = offset;\n }\n\n /**\n * Start sources for clips that span the given Transport position.\n * Used for mid-playback seeking and loop boundary handling where\n * Transport.schedule() callbacks have already passed.\n *\n * Uses strict < for absClipStart to avoid double-creation with\n * schedule callbacks at exact Transport position (e.g., loopStart).\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo, fadeGainNode } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n // Only handle clips that started before the transport position\n // but haven't ended yet (i.e., the transport is \"inside\" the clip)\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n const elapsed = transportOffset - absClipStart;\n const adjustedOffset = clipInfo.offset + elapsed;\n const remainingDuration = clipInfo.duration - elapsed;\n this.startClipSource(\n clipInfo,\n fadeGainNode,\n audioContextTime,\n adjustedOffset,\n remainingDuration\n );\n }\n }\n }\n\n /**\n * Stop all active AudioBufferSourceNodes and clear the set.\n * Native AudioBufferSourceNodes ignore Transport state changes —\n * they must be explicitly stopped.\n */\n stopAllSources(): void {\n this.activeSources.forEach((source) => {\n try {\n source.stop();\n } catch (err) {\n console.warn(`[waveform-playlist] Error stopping source on track \"${this.id}\":`, err);\n }\n });\n this.activeSources.clear();\n }\n\n /**\n * Schedule fade envelopes for a clip at the given AudioContext time.\n * Uses native GainNode.gain (AudioParam) directly — no _param workaround needed.\n */\n private scheduleFades(\n scheduled: ScheduledClip,\n clipStartTime: number,\n clipOffset: number = 0\n ): void {\n const { clipInfo, fadeGainNode } = scheduled;\n const audioParam = fadeGainNode.gain;\n\n // Cancel any previous automation\n audioParam.cancelScheduledValues(0);\n\n // Calculate how much of the clip we're skipping (for seeking)\n const skipTime = clipOffset - clipInfo.offset;\n\n // Apply fade in if it exists and we haven't skipped past it\n if (clipInfo.fadeIn && skipTime < clipInfo.fadeIn.duration) {\n const fadeInDuration = clipInfo.fadeIn.duration;\n\n if (skipTime <= 0) {\n applyFadeIn(\n audioParam,\n clipStartTime,\n fadeInDuration,\n clipInfo.fadeIn.type || 'linear',\n 0,\n clipInfo.gain\n );\n } else {\n const remainingFadeDuration = fadeInDuration - skipTime;\n const fadeProgress = skipTime / fadeInDuration;\n const startValue = clipInfo.gain * fadeProgress;\n applyFadeIn(\n audioParam,\n clipStartTime,\n remainingFadeDuration,\n clipInfo.fadeIn.type || 'linear',\n startValue,\n clipInfo.gain\n );\n }\n } else {\n audioParam.setValueAtTime(clipInfo.gain, clipStartTime);\n }\n\n // Apply fade out if it exists\n if (clipInfo.fadeOut) {\n const fadeOutStart = clipInfo.duration - clipInfo.fadeOut.duration;\n const fadeOutStartInClip = fadeOutStart - skipTime;\n\n if (fadeOutStartInClip > 0) {\n const absoluteFadeOutStart = clipStartTime + fadeOutStartInClip;\n applyFadeOut(\n audioParam,\n absoluteFadeOutStart,\n clipInfo.fadeOut.duration,\n clipInfo.fadeOut.type || 'linear',\n clipInfo.gain,\n 0\n );\n } else if (fadeOutStartInClip > -clipInfo.fadeOut.duration) {\n const elapsedFadeOut = -fadeOutStartInClip;\n const remainingFadeDuration = clipInfo.fadeOut.duration - elapsedFadeOut;\n const fadeProgress = elapsedFadeOut / clipInfo.fadeOut.duration;\n const startValue = clipInfo.gain * (1 - fadeProgress);\n applyFadeOut(\n audioParam,\n clipStartTime,\n remainingFadeDuration,\n clipInfo.fadeOut.type || 'linear',\n startValue,\n 0\n );\n }\n }\n }\n\n /**\n * Prepare fade envelopes for all clips based on Transport offset.\n * Called before Transport.start() to schedule fades at correct AudioContext times.\n */\n prepareFades(when: number, transportOffset: number): void {\n this.scheduledClips.forEach((scheduled) => {\n const absClipStart = this.track.startTime + scheduled.clipInfo.startTime;\n const absClipEnd = absClipStart + scheduled.clipInfo.duration;\n\n if (transportOffset >= absClipEnd) return; // clip already finished\n\n if (transportOffset >= absClipStart) {\n // Mid-clip: playing now\n const clipOffset = transportOffset - absClipStart + scheduled.clipInfo.offset;\n this.scheduleFades(scheduled, when, clipOffset);\n } else {\n // Clip starts later\n const delay = absClipStart - transportOffset;\n this.scheduleFades(scheduled, when + delay, scheduled.clipInfo.offset);\n }\n });\n }\n\n /**\n * Cancel all scheduled fade automation and reset to nominal gain.\n * Called on pause/stop to prevent stale fade envelopes.\n */\n cancelFades(): void {\n this.scheduledClips.forEach(({ fadeGainNode, clipInfo }) => {\n const audioParam = fadeGainNode.gain;\n audioParam.cancelScheduledValues(0);\n audioParam.setValueAtTime(clipInfo.gain, 0);\n });\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n // Use setValueAtTime on the raw AudioParam to ensure the value is applied\n // even when the AudioContext is suspended. Setting .gain.value on the Tone.js\n // Signal wrapper doesn't propagate to the underlying AudioParam until the\n // context resumes, causing a brief audio glitch (e.g., all tracks audible\n // before solo muting takes effect).\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n const transport = getTransport();\n\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(`[waveform-playlist] Error during track \"${this.id}\" effects cleanup:`, err);\n }\n }\n\n this.stopAllSources();\n\n // Clear Transport schedule events and disconnect native fade gain nodes\n this.scheduledClips.forEach((scheduled, index) => {\n try {\n transport.clear(scheduled.scheduleId);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error clearing schedule ${index} on track \"${this.id}\":`,\n err\n );\n }\n try {\n scheduled.fadeGainNode.disconnect();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disconnecting fadeGain ${index} on track \"${this.id}\":`,\n err\n );\n }\n });\n\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing volumeNode on track \"${this.id}\":`, err);\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing panNode on track \"${this.id}\":`, err);\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing muteGain on track \"${this.id}\":`, err);\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get buffer(): AudioBuffer {\n return this.scheduledClips[0]?.clipInfo.buffer;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","/**\n * Fade utilities — re-exports from @waveform-playlist/core\n * plus Tone.js-specific helpers\n */\n\n// Re-export all pure fade utilities from core\nexport {\n linearCurve,\n exponentialCurve,\n sCurveCurve,\n logarithmicCurve,\n generateCurve,\n applyFadeIn,\n applyFadeOut,\n} from '@waveform-playlist/core';\n\nexport type { FadeType, FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Access the underlying Web Audio AudioParam from a Tone.js Signal/Param wrapper.\n *\n * Tone.js wraps native AudioParam in its Signal class, but sometimes we need\n * direct access to the raw AudioParam for setValueAtTime/cancelScheduledValues\n * (e.g., when the AudioContext is suspended and Tone.js Signal doesn't propagate).\n *\n * This uses `_param` which is a private Tone.js 15.x internal.\n * Pin the Tone.js version carefully if upgrading.\n *\n * @param signal - A Tone.js Signal or Param wrapper (e.g., `gain.gain`)\n * @returns The underlying AudioParam, or undefined if not found\n */\nlet hasWarned = false;\n\nexport function getUnderlyingAudioParam(signal: unknown): AudioParam | undefined {\n const param = (signal as { _param?: AudioParam })._param;\n if (!param && !hasWarned) {\n hasWarned = true;\n console.warn(\n '[waveform-playlist] Unable to access Tone.js internal _param. ' +\n 'This likely means the Tone.js version is incompatible. ' +\n 'Mute scheduling may not work correctly.'\n );\n }\n return param;\n}\n","import {\n Volume,\n Gain,\n Panner,\n PolySynth,\n Synth,\n MembraneSynth,\n MetalSynth,\n NoiseSynth,\n Part,\n ToneAudioNode,\n getDestination,\n getContext,\n} from 'tone';\nimport type { SynthOptions } from 'tone';\nimport { Track } from '@waveform-playlist/core';\nimport type { MidiNoteData } from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam } from './fades';\nimport type { TrackEffectsFunction } from './ToneTrack';\n\n/**\n * Shared interface for tracks managed by TonePlayout.\n * Both ToneTrack (audio) and MidiToneTrack (MIDI) implement this,\n * allowing TonePlayout to manage them uniformly.\n */\nexport interface PlayableTrack {\n id: string;\n startTime: number;\n muted: boolean;\n duration: number;\n stopAllSources(): void;\n startMidClipSources(offset: number, time: number): void;\n setScheduleGuardOffset(offset: number): void;\n prepareFades(when: number, offset: number): void;\n cancelFades(): void;\n setVolume(gain: number): void;\n setPan(pan: number): void;\n setMute(muted: boolean): void;\n setSolo(soloed: boolean): void;\n dispose(): void;\n}\n\nexport interface MidiClipInfo {\n notes: MidiNoteData[];\n startTime: number; // When this clip starts relative to track start (seconds)\n duration: number; // Clip duration (seconds)\n offset: number; // Trim offset into the MIDI data (seconds)\n}\n\nexport interface MidiToneTrackOptions {\n clips: MidiClipInfo[];\n track: Track;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n synthOptions?: Partial<SynthOptions>;\n}\n\n/**\n * Categorize GM percussion note numbers into synth types.\n * See: https://www.midi.org/specifications-old/item/gm-level-1-sound-set\n */\ntype DrumCategory = 'kick' | 'snare' | 'cymbal' | 'tom';\n\nfunction getDrumCategory(midiNote: number): DrumCategory {\n // Bass drums\n if (midiNote === 35 || midiNote === 36) return 'kick';\n // Snare, side stick, clap\n if (midiNote >= 37 && midiNote <= 40) return 'snare';\n // Toms (low floor tom through high tom)\n if (\n midiNote === 41 ||\n midiNote === 43 ||\n midiNote === 45 ||\n midiNote === 47 ||\n midiNote === 48 ||\n midiNote === 50\n )\n return 'tom';\n // Hi-hats, cymbals, bells, tambourine, cowbell, etc.\n return 'cymbal';\n}\n\n/** Per-clip scheduling info */\ninterface ScheduledMidiClip {\n clipInfo: MidiClipInfo;\n part: Part;\n}\n\n/**\n * MIDI track that always creates both melodic and percussion synths.\n * Per-note routing uses the `channel` field on each MidiNoteData:\n * channel 9 → percussion synths, all others → melodic PolySynth.\n * This enables flattened tracks (mixed channels) to play correctly.\n */\nexport class MidiToneTrack implements PlayableTrack {\n private scheduledClips: ScheduledMidiClip[];\n // Melodic synth — always created\n private synth: PolySynth;\n // Percussion synths — always created (PolySynth wrappers for polyphony)\n private kickSynth: PolySynth<MembraneSynth>;\n private snareSynth: NoiseSynth; // No pitch param, can't wrap in PolySynth\n private cymbalSynth: PolySynth<MetalSynth>;\n private tomSynth: PolySynth<MembraneSynth>;\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n\n constructor(options: MidiToneTrackOptions) {\n this.track = options.track;\n\n // Create shared track-level Tone.js nodes (same chain as ToneTrack)\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n this.panNode = new Panner(options.track.stereoPan);\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Melodic: PolySynth with basic Synth voice\n this.synth = new PolySynth(Synth, options.synthOptions);\n this.synth.connect(this.volumeNode);\n\n // Percussion: PolySynth wrappers for polyphonic playback\n this.kickSynth = new PolySynth(MembraneSynth, {\n voice: MembraneSynth,\n options: {\n pitchDecay: 0.05,\n octaves: 6,\n envelope: { attack: 0.001, decay: 0.4, sustain: 0, release: 0.1 },\n },\n } as never);\n this.snareSynth = new NoiseSynth({\n noise: { type: 'white' },\n envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.05 },\n });\n this.cymbalSynth = new PolySynth(MetalSynth, {\n voice: MetalSynth,\n options: {\n envelope: { attack: 0.001, decay: 0.3, release: 0.1 },\n harmonicity: 5.1,\n modulationIndex: 32,\n resonance: 4000,\n octaves: 1.5,\n },\n } as never);\n this.tomSynth = new PolySynth(MembraneSynth, {\n voice: MembraneSynth,\n options: {\n pitchDecay: 0.08,\n octaves: 4,\n envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.1 },\n },\n } as never);\n\n this.kickSynth.connect(this.volumeNode);\n this.snareSynth.connect(this.volumeNode);\n this.cymbalSynth.connect(this.volumeNode);\n this.tomSynth.connect(this.volumeNode);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create a Tone.Part for each clip, scheduling notes relative to Transport\n this.scheduledClips = options.clips.map((clipInfo) => {\n // Filter notes within the clip's visible window (after trim offset)\n const visibleNotes = clipInfo.notes.filter((note) => {\n const noteEnd = note.time + note.duration;\n // Note must start before clip ends and end after clip's trim offset\n return note.time < clipInfo.offset + clipInfo.duration && noteEnd > clipInfo.offset;\n });\n\n // Create Part events with absolute Transport times\n const absClipStart = this.track.startTime + clipInfo.startTime;\n\n const partEvents = visibleNotes.map((note) => {\n // Adjust note timing relative to clip's trim offset\n const adjustedTime = note.time - clipInfo.offset;\n // Clamp to clip boundaries\n const clampedStart = Math.max(0, adjustedTime);\n const clampedDuration = Math.min(\n note.duration - Math.max(0, clipInfo.offset - note.time),\n clipInfo.duration - clampedStart\n );\n\n return {\n time: absClipStart + clampedStart,\n note: note.name,\n midi: note.midi,\n duration: Math.max(0, clampedDuration),\n velocity: note.velocity,\n channel: note.channel,\n };\n });\n\n const part = new Part((time, event) => {\n if (event.duration > 0) {\n this.triggerNote(\n event.midi,\n event.note,\n event.duration,\n time,\n event.velocity,\n event.channel\n );\n }\n }, partEvents);\n\n // Part starts automatically with Transport\n part.start(0);\n\n return { clipInfo, part };\n });\n }\n\n /**\n * Trigger a note using the appropriate synth.\n * Routes per-note: channel 9 → percussion synths, others → melodic PolySynth.\n */\n private triggerNote(\n midiNote: number,\n noteName: string,\n duration: number,\n time: number,\n velocity: number,\n channel?: number\n ): void {\n if (channel === 9) {\n const category = getDrumCategory(midiNote);\n switch (category) {\n case 'kick':\n this.kickSynth.triggerAttackRelease('C1', duration, time, velocity);\n break;\n case 'snare':\n // NoiseSynth is monophonic — wrap in try-catch for rare overlaps\n try {\n this.snareSynth.triggerAttackRelease(duration, time, velocity);\n } catch (err) {\n console.warn(\n '[waveform-playlist] Snare overlap — previous hit still decaying, skipped:',\n err\n );\n }\n break;\n case 'tom': {\n const tomPitches: Record<number, string> = {\n 41: 'G1',\n 43: 'A1',\n 45: 'C2',\n 47: 'D2',\n 48: 'E2',\n 50: 'G2',\n };\n this.tomSynth.triggerAttackRelease(\n tomPitches[midiNote] || 'C2',\n duration,\n time,\n velocity\n );\n break;\n }\n case 'cymbal':\n // PolySynth requires a note arg; MetalSynth uses it as fundamental frequency.\n // 'C4' provides a reasonable metallic timbre for all cymbal/hi-hat hits.\n this.cymbalSynth.triggerAttackRelease('C4', duration, time, velocity);\n break;\n }\n } else {\n this.synth.triggerAttackRelease(noteName, duration, time, velocity);\n }\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n /**\n * No-op for MIDI — schedule guard is for AudioBufferSourceNode ghost tick prevention.\n * Tone.Part handles its own scheduling relative to Transport.\n */\n setScheduleGuardOffset(_offset: number): void {\n // No-op: Tone.Part handles scheduling internally\n }\n\n /**\n * For MIDI, mid-clip sources are notes that should already be sounding.\n * We trigger them with their remaining duration.\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n // Find notes that should be currently sounding\n for (const note of clipInfo.notes) {\n const adjustedTime = note.time - clipInfo.offset;\n const noteAbsStart = absClipStart + Math.max(0, adjustedTime);\n const noteAbsEnd = noteAbsStart + note.duration;\n\n if (noteAbsStart < transportOffset && noteAbsEnd > transportOffset) {\n const remainingDuration = noteAbsEnd - transportOffset;\n try {\n this.triggerNote(\n note.midi,\n note.name,\n remainingDuration,\n audioContextTime,\n note.velocity,\n note.channel\n );\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start mid-clip MIDI note \"${note.name}\" on track \"${this.id}\":`,\n err\n );\n }\n }\n }\n }\n }\n }\n\n /**\n * Stop all sounding notes and cancel scheduled Part events.\n */\n stopAllSources(): void {\n const now = getContext().rawContext.currentTime;\n try {\n this.synth.releaseAll(now);\n this.kickSynth.releaseAll(now);\n this.cymbalSynth.releaseAll(now);\n this.tomSynth.releaseAll(now);\n // NoiseSynth has no releaseAll — it decays naturally via short envelope\n } catch (err) {\n console.warn(`[waveform-playlist] Error releasing synth on track \"${this.id}\":`, err);\n }\n }\n\n /**\n * No-op for MIDI — MIDI uses note velocity, not gain fades.\n */\n prepareFades(_when: number, _offset: number): void {\n // No-op: MIDI clips don't use fade envelopes\n }\n\n /**\n * No-op for MIDI — no fade automation to cancel.\n */\n cancelFades(): void {\n // No-op: MIDI clips don't use fade envelopes\n }\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error during MIDI track \"${this.id}\" effects cleanup:`,\n err\n );\n }\n }\n\n this.stopAllSources();\n\n // Dispose Parts\n this.scheduledClips.forEach(({ part }, index) => {\n try {\n part.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing Part ${index} on MIDI track \"${this.id}\":`,\n err\n );\n }\n });\n\n // Dispose all synths\n const synthsToDispose = [\n this.synth,\n this.kickSynth,\n this.snareSynth,\n this.cymbalSynth,\n this.tomSynth,\n ];\n for (const s of synthsToDispose) {\n try {\n s?.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing synth on MIDI track \"${this.id}\":`, err);\n }\n }\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing volumeNode on MIDI track \"${this.id}\":`,\n err\n );\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing panNode on MIDI track \"${this.id}\":`, err);\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing muteGain on MIDI track \"${this.id}\":`, err);\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","import { Volume, Gain, Panner, Part, ToneAudioNode, getDestination, getContext } from 'tone';\nimport { Track } from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam } from './fades';\nimport type { TrackEffectsFunction } from './ToneTrack';\nimport type { PlayableTrack, MidiClipInfo } from './MidiToneTrack';\nimport type { SoundFontCache } from './SoundFontCache';\n\nexport interface SoundFontToneTrackOptions {\n clips: MidiClipInfo[];\n track: Track;\n soundFontCache: SoundFontCache;\n /** GM program number (0-127) for melodic instruments */\n programNumber?: number;\n /** Whether this track uses percussion bank (channel 9) */\n isPercussion?: boolean;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n}\n\n/** Per-clip scheduling info */\ninterface ScheduledMidiClip {\n clipInfo: MidiClipInfo;\n part: Part;\n}\n\n/**\n * MIDI track that uses SoundFont samples for playback.\n *\n * Instead of PolySynth synthesis, each note triggers the correct instrument\n * sample from an SF2 file, pitch-shifted via AudioBufferSourceNode.playbackRate.\n *\n * Audio graph per note:\n * AudioBufferSourceNode (native, one-shot, pitch-shifted)\n * → GainNode (native, per-note velocity)\n * → Volume.input (Tone.js, shared per-track)\n * → Panner → muteGain → effects/destination\n */\nexport class SoundFontToneTrack implements PlayableTrack {\n /** Rate-limit missing sample warnings — one per class lifetime */\n private static _missingSampleWarned = false;\n private scheduledClips: ScheduledMidiClip[];\n private activeSources: Set<AudioBufferSourceNode> = new Set();\n private soundFontCache: SoundFontCache;\n private programNumber: number;\n private bankNumber: number;\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n\n constructor(options: SoundFontToneTrackOptions) {\n this.track = options.track;\n this.soundFontCache = options.soundFontCache;\n this.programNumber = options.programNumber ?? 0;\n // Bank 128 for percussion (channel 9), bank 0 for melodic\n this.bankNumber = options.isPercussion ? 128 : 0;\n\n // Create shared track-level Tone.js nodes (same chain as ToneTrack)\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n this.panNode = new Panner(options.track.stereoPan);\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create a Tone.Part for each clip, scheduling notes relative to Transport\n this.scheduledClips = options.clips.map((clipInfo) => {\n // Filter notes within the clip's visible window (after trim offset)\n const visibleNotes = clipInfo.notes.filter((note) => {\n const noteEnd = note.time + note.duration;\n return note.time < clipInfo.offset + clipInfo.duration && noteEnd > clipInfo.offset;\n });\n\n const absClipStart = this.track.startTime + clipInfo.startTime;\n\n const partEvents = visibleNotes.map((note) => {\n const adjustedTime = note.time - clipInfo.offset;\n const clampedStart = Math.max(0, adjustedTime);\n const clampedDuration = Math.min(\n note.duration - Math.max(0, clipInfo.offset - note.time),\n clipInfo.duration - clampedStart\n );\n\n return {\n time: absClipStart + clampedStart,\n note: note.name,\n midi: note.midi,\n duration: Math.max(0, clampedDuration),\n velocity: note.velocity,\n channel: note.channel,\n };\n });\n\n const part = new Part((time, event) => {\n if (event.duration > 0) {\n this.triggerNote(event.midi, event.duration, time, event.velocity, event.channel);\n }\n }, partEvents);\n\n part.start(0);\n\n return { clipInfo, part };\n });\n }\n\n /**\n * Trigger a note by creating a native AudioBufferSourceNode from the SoundFont cache.\n *\n * Per-note routing: channel 9 → bank 128 (drums), others → bank 0 with programNumber.\n */\n private triggerNote(\n midiNote: number,\n duration: number,\n time: number,\n velocity: number,\n channel?: number\n ): void {\n const bank = channel === 9 ? 128 : this.bankNumber;\n const preset = channel === 9 ? 0 : this.programNumber;\n\n const sfSample = this.soundFontCache.getAudioBuffer(midiNote, bank, preset);\n if (!sfSample) {\n if (!SoundFontToneTrack._missingSampleWarned) {\n console.warn(\n `[waveform-playlist] SoundFont sample not found for MIDI note ${midiNote} (bank ${bank}, preset ${preset}). ` +\n 'Subsequent missing samples will be silent.'\n );\n SoundFontToneTrack._missingSampleWarned = true;\n }\n return;\n }\n\n const rawContext = getContext().rawContext as BaseAudioContext;\n\n // Create AudioBufferSourceNode with optional looping\n const source = rawContext.createBufferSource();\n source.buffer = sfSample.buffer;\n source.playbackRate.value = sfSample.playbackRate;\n\n // Enable sample looping if SF2 zone defines loop mode\n if (sfSample.loopMode === 1 || sfSample.loopMode === 3) {\n source.loop = true;\n source.loopStart = sfSample.loopStart;\n source.loopEnd = sfSample.loopEnd;\n }\n\n // For non-looping samples (percussion, one-shots), use the sample's natural\n // buffer duration so cymbals/drums ring out fully. For looping samples\n // (sustained instruments), use the MIDI note duration for note-off timing.\n const sampleDuration = sfSample.buffer.duration / sfSample.playbackRate;\n const effectiveDuration =\n sfSample.loopMode === 0 ? Math.max(duration, sampleDuration) : duration;\n\n // Per-note gain with SF2 volume ADSR envelope\n const peakGain = velocity * velocity;\n const gainNode = rawContext.createGain();\n const { attackVolEnv, holdVolEnv, decayVolEnv, sustainVolEnv, releaseVolEnv } = sfSample;\n const sustainGain = peakGain * sustainVolEnv;\n\n // Attack: start silent, ramp to peak\n gainNode.gain.setValueAtTime(0, time);\n gainNode.gain.linearRampToValueAtTime(peakGain, time + attackVolEnv);\n // Hold at peak\n if (holdVolEnv > 0.001) {\n gainNode.gain.setValueAtTime(peakGain, time + attackVolEnv + holdVolEnv);\n }\n // Decay to sustain level\n const decayStart = time + attackVolEnv + holdVolEnv;\n gainNode.gain.linearRampToValueAtTime(sustainGain, decayStart + decayVolEnv);\n // Sustain holds until note-off, then release ramps to silence.\n // For short notes (duration < AHD), setValueAtTime at note-off cancels the\n // incomplete decay ramp — Web Audio handles this correctly.\n gainNode.gain.setValueAtTime(sustainGain, time + effectiveDuration);\n gainNode.gain.linearRampToValueAtTime(0, time + effectiveDuration + releaseVolEnv);\n\n // Connect: source → gainNode → Volume.input (Tone.js)\n source.connect(gainNode);\n gainNode.connect((this.volumeNode.input as unknown as Gain).input);\n\n // Track active sources for stopAllSources()\n this.activeSources.add(source);\n source.onended = () => {\n this.activeSources.delete(source);\n try {\n gainNode.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] GainNode already disconnected:', err);\n }\n };\n\n source.start(time);\n source.stop(time + effectiveDuration + releaseVolEnv);\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n /**\n * No-op — Tone.Part handles scheduling internally, no ghost tick guard needed.\n */\n setScheduleGuardOffset(_offset: number): void {\n // No-op\n }\n\n /**\n * Start notes that should already be sounding at the current transport offset.\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n for (const note of clipInfo.notes) {\n const adjustedTime = note.time - clipInfo.offset;\n const noteAbsStart = absClipStart + Math.max(0, adjustedTime);\n const noteAbsEnd = noteAbsStart + note.duration;\n\n if (noteAbsStart < transportOffset && noteAbsEnd > transportOffset) {\n const remainingDuration = noteAbsEnd - transportOffset;\n try {\n this.triggerNote(\n note.midi,\n remainingDuration,\n audioContextTime,\n note.velocity,\n note.channel\n );\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start mid-clip SoundFont note on track \"${this.id}\":`,\n err\n );\n }\n }\n }\n }\n }\n }\n\n /**\n * Stop all active AudioBufferSourceNodes.\n */\n stopAllSources(): void {\n for (const source of this.activeSources) {\n try {\n source.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Error stopping AudioBufferSourceNode:', err);\n }\n }\n this.activeSources.clear();\n }\n\n /** No-op for MIDI — MIDI uses note velocity, not gain fades. */\n prepareFades(_when: number, _offset: number): void {}\n\n /** No-op for MIDI — no fade automation to cancel. */\n cancelFades(): void {}\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error during SoundFont track \"${this.id}\" effects cleanup:`,\n err\n );\n }\n }\n\n this.stopAllSources();\n\n // Dispose Parts\n this.scheduledClips.forEach(({ part }, index) => {\n try {\n part.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing Part ${index} on SoundFont track \"${this.id}\":`,\n err\n );\n }\n });\n\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing volumeNode on SoundFont track \"${this.id}\":`,\n err\n );\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing panNode on SoundFont track \"${this.id}\":`,\n err\n );\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing muteGain on SoundFont track \"${this.id}\":`,\n err\n );\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","import { SoundFont2, GeneratorType } from 'soundfont2';\nimport type { Generator, ZoneMap } from 'soundfont2';\n\n/**\n * Result of looking up a MIDI note in the SoundFont.\n * Contains the AudioBuffer, playbackRate, loop points, and volume envelope.\n */\nexport interface SoundFontSample {\n /** Cached AudioBuffer for this sample */\n buffer: AudioBuffer;\n /** Playback rate to pitch-shift from originalPitch to target note */\n playbackRate: number;\n /** Loop mode: 0=no loop, 1=continuous, 3=sustain loop */\n loopMode: number;\n /** Loop start in seconds, relative to AudioBuffer start */\n loopStart: number;\n /** Loop end in seconds, relative to AudioBuffer start */\n loopEnd: number;\n /** Volume envelope attack time in seconds */\n attackVolEnv: number;\n /** Volume envelope hold time in seconds */\n holdVolEnv: number;\n /** Volume envelope decay time in seconds */\n decayVolEnv: number;\n /** Volume envelope sustain level as linear gain 0-1 */\n sustainVolEnv: number;\n /** Volume envelope release time in seconds */\n releaseVolEnv: number;\n}\n\n/**\n * Convert SF2 timecents to seconds.\n * SF2 formula: seconds = 2^(timecents / 1200)\n * Default -12000 timecents ≈ 0.001s (effectively instant).\n */\nexport function timecentsToSeconds(tc: number): number {\n return Math.pow(2, tc / 1200);\n}\n\n/** Max release time to prevent extremely long tails from stale generators */\nconst MAX_RELEASE_SECONDS = 5;\n\n/**\n * Get a numeric generator value from a zone map.\n */\nexport function getGeneratorValue(\n generators: ZoneMap<Generator>,\n type: GeneratorType\n): number | undefined {\n return generators[type]?.value;\n}\n\n/**\n * Convert Int16Array sample data to Float32Array.\n * SF2 samples are 16-bit signed integers; Web Audio needs Float32 [-1, 1].\n */\nexport function int16ToFloat32(samples: Int16Array): Float32Array {\n const floats = new Float32Array(samples.length);\n for (let i = 0; i < samples.length; i++) {\n floats[i] = samples[i] / 32768;\n }\n return floats;\n}\n\n/**\n * Input parameters for playback rate calculation.\n */\nexport interface PlaybackRateParams {\n /** Target MIDI note number (0-127) */\n midiNote: number;\n /** OverridingRootKey generator value, or undefined if not set */\n overrideRootKey: number | undefined;\n /** sample.header.originalPitch (255 means unpitched) */\n originalPitch: number;\n /** CoarseTune generator value in semitones (default 0) */\n coarseTune: number;\n /** FineTune generator value in cents (default 0) */\n fineTune: number;\n /** sample.header.pitchCorrection in cents (default 0) */\n pitchCorrection: number;\n}\n\n/**\n * Calculate playback rate for a MIDI note using the SF2 generator chain.\n *\n * SF2 root key resolution priority:\n * 1. OverridingRootKey generator (per-zone, most specific)\n * 2. sample.header.originalPitch (sample header)\n * 3. MIDI note 60 (middle C fallback)\n *\n * Tuning adjustments:\n * - CoarseTune generator (semitones, additive)\n * - FineTune generator (cents, additive)\n * - sample.header.pitchCorrection (cents, additive)\n */\nexport function calculatePlaybackRate(params: PlaybackRateParams): number {\n const { midiNote, overrideRootKey, originalPitch, coarseTune, fineTune, pitchCorrection } =\n params;\n\n // Resolve root key: OverridingRootKey → originalPitch → 60\n const rootKey =\n overrideRootKey !== undefined ? overrideRootKey : originalPitch !== 255 ? originalPitch : 60;\n\n // Total offset in semitones: target note - root key + tuning\n const totalSemitones = midiNote - rootKey + coarseTune + (fineTune + pitchCorrection) / 100;\n\n return Math.pow(2, totalSemitones / 12);\n}\n\n/**\n * Input parameters for loop and envelope extraction.\n */\nexport interface LoopAndEnvelopeParams {\n /** SF2 generators zone map */\n generators: ZoneMap<Generator>;\n /** Sample header with loop points and sample rate */\n header: {\n startLoop: number;\n endLoop: number;\n sampleRate: number;\n };\n}\n\n/**\n * Extract loop points and volume envelope data from per-zone generators.\n *\n * Loop points are stored as absolute indices into the SF2 sample pool.\n * We convert to AudioBuffer-relative seconds by subtracting header.start\n * and dividing by sampleRate.\n *\n * Volume envelope times are in SF2 timecents; sustain is centibels attenuation.\n */\nexport function extractLoopAndEnvelope(\n params: LoopAndEnvelopeParams\n): Omit<SoundFontSample, 'buffer' | 'playbackRate'> {\n const { generators, header } = params;\n\n // --- Loop points ---\n const loopMode = getGeneratorValue(generators, GeneratorType.SampleModes) ?? 0;\n\n // Compute actual loop positions (header + fine/coarse generator offsets)\n const rawLoopStart =\n header.startLoop +\n (getGeneratorValue(generators, GeneratorType.StartLoopAddrsOffset) ?? 0) +\n (getGeneratorValue(generators, GeneratorType.StartLoopAddrsCoarseOffset) ?? 0) * 32768;\n const rawLoopEnd =\n header.endLoop +\n (getGeneratorValue(generators, GeneratorType.EndLoopAddrsOffset) ?? 0) +\n (getGeneratorValue(generators, GeneratorType.EndLoopAddrsCoarseOffset) ?? 0) * 32768;\n\n // The soundfont2 library already converts startLoop/endLoop to be\n // relative to sample.data (subtracts header.start during parsing),\n // so we only need to divide by sampleRate to get seconds.\n const loopStart = rawLoopStart / header.sampleRate;\n const loopEnd = rawLoopEnd / header.sampleRate;\n\n // --- Volume envelope ---\n const attackVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.AttackVolEnv) ?? -12000\n );\n const holdVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.HoldVolEnv) ?? -12000\n );\n const decayVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.DecayVolEnv) ?? -12000\n );\n const releaseVolEnv = Math.min(\n timecentsToSeconds(getGeneratorValue(generators, GeneratorType.ReleaseVolEnv) ?? -12000),\n MAX_RELEASE_SECONDS\n );\n\n // SustainVolEnv is centibels attenuation: 0 = full volume, 1440 = silence\n // Convert to linear gain: 10^(-cb / 200)\n const sustainCb = getGeneratorValue(generators, GeneratorType.SustainVolEnv) ?? 0;\n const sustainVolEnv = Math.pow(10, -sustainCb / 200);\n\n return {\n loopMode,\n loopStart,\n loopEnd,\n attackVolEnv,\n holdVolEnv,\n decayVolEnv,\n sustainVolEnv,\n releaseVolEnv,\n };\n}\n\n/**\n * Caches parsed SoundFont2 data and AudioBuffers for efficient playback.\n *\n * AudioBuffers are created lazily on first access and cached by sample index.\n * Pitch calculation uses the SF2 generator chain:\n * OverridingRootKey → sample.header.originalPitch → fallback 60\n *\n * Audio graph per note:\n * AudioBufferSourceNode (playbackRate for pitch) → GainNode (velocity) → track chain\n */\nexport class SoundFontCache {\n private sf2: SoundFont2 | null = null;\n private audioBufferCache: Map<number, AudioBuffer> = new Map();\n private context: BaseAudioContext;\n\n /**\n * @param context Optional AudioContext for createBuffer(). If omitted, uses\n * an OfflineAudioContext which doesn't require user gesture — safe to\n * construct before user interaction (avoids Firefox autoplay warnings).\n */\n constructor(context?: BaseAudioContext) {\n // OfflineAudioContext only needs valid params; we never call startRendering().\n // It's used solely for createBuffer() which works identically to AudioContext.\n this.context = context ?? new OfflineAudioContext(1, 1, 44100);\n }\n\n /**\n * Load and parse an SF2 file from a URL.\n */\n async load(url: string, signal?: AbortSignal): Promise<void> {\n const response = await fetch(url, { signal });\n if (!response.ok) {\n throw new Error(`Failed to fetch SoundFont ${url}: ${response.statusText}`);\n }\n const arrayBuffer = await response.arrayBuffer();\n try {\n this.sf2 = new SoundFont2(new Uint8Array(arrayBuffer));\n } catch (err) {\n throw new Error(\n `Failed to parse SoundFont ${url}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n /**\n * Load from an already-fetched ArrayBuffer.\n */\n loadFromBuffer(data: ArrayBuffer): void {\n try {\n this.sf2 = new SoundFont2(new Uint8Array(data));\n } catch (err) {\n throw new Error(\n `Failed to parse SoundFont from buffer: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n get isLoaded(): boolean {\n return this.sf2 !== null;\n }\n\n /**\n * Look up a MIDI note and return the AudioBuffer + playbackRate.\n *\n * @param midiNote - MIDI note number (0-127)\n * @param bankNumber - Bank number (0 for melodic, 128 for percussion/drums)\n * @param presetNumber - GM program number (0-127)\n * @returns SoundFontSample or null if no sample found for this note\n */\n getAudioBuffer(\n midiNote: number,\n bankNumber: number = 0,\n presetNumber: number = 0\n ): SoundFontSample | null {\n if (!this.sf2) return null;\n\n const keyData = this.sf2.getKeyData(midiNote, bankNumber, presetNumber);\n if (!keyData) return null;\n\n const sample = keyData.sample;\n const sampleIndex = this.sf2.samples.indexOf(sample);\n\n // Get or create the AudioBuffer for this sample\n let buffer = this.audioBufferCache.get(sampleIndex);\n if (!buffer) {\n buffer = this.int16ToAudioBuffer(sample.data, sample.header.sampleRate);\n this.audioBufferCache.set(sampleIndex, buffer);\n }\n\n // Calculate playback rate using SF2 generator chain for root key.\n // Priority: OverridingRootKey generator → sample.header.originalPitch → 60\n const playbackRate = calculatePlaybackRate({\n midiNote,\n overrideRootKey: getGeneratorValue(keyData.generators, GeneratorType.OverridingRootKey),\n originalPitch: sample.header.originalPitch,\n coarseTune: getGeneratorValue(keyData.generators, GeneratorType.CoarseTune) ?? 0,\n fineTune: getGeneratorValue(keyData.generators, GeneratorType.FineTune) ?? 0,\n pitchCorrection: sample.header.pitchCorrection ?? 0,\n });\n\n // Extract per-zone loop points and volume envelope from generators\n const loopAndEnvelope = extractLoopAndEnvelope({\n generators: keyData.generators,\n header: keyData.sample.header,\n });\n\n return { buffer, playbackRate, ...loopAndEnvelope };\n }\n\n /**\n * Convert Int16Array sample data to an AudioBuffer.\n * Uses the extracted int16ToFloat32 for the conversion, then copies into an AudioBuffer.\n */\n private int16ToAudioBuffer(data: Int16Array, sampleRate: number): AudioBuffer {\n const floats = int16ToFloat32(data);\n const buffer = this.context.createBuffer(1, floats.length, sampleRate);\n buffer.getChannelData(0).set(floats);\n return buffer;\n }\n\n /**\n * Clear all cached AudioBuffers and release the parsed SF2.\n */\n dispose(): void {\n this.audioBufferCache.clear();\n this.sf2 = null;\n }\n}\n","/**\n * Global AudioContext Manager\n *\n * Provides a single AudioContext shared across the entire application.\n * This context is used by Tone.js for playback and by all recording/monitoring hooks.\n *\n * Uses Tone.js's Context class which wraps standardized-audio-context for\n * cross-browser compatibility (fixes Firefox AudioListener issues).\n */\n\nimport { Context, setContext } from 'tone';\n\nlet globalToneContext: Context | null = null;\n\n/**\n * Get the global Tone.js Context\n * This is the main context for cross-browser audio operations.\n * Use context.createAudioWorkletNode(), context.createMediaStreamSource(), etc.\n * @returns The Tone.js Context instance\n */\nexport function getGlobalContext(): Context {\n if (!globalToneContext) {\n globalToneContext = new Context();\n setContext(globalToneContext);\n }\n return globalToneContext;\n}\n\n/**\n * Get or create the global AudioContext\n * Uses Tone.js Context for cross-browser compatibility\n * @returns The global AudioContext instance (rawContext from Tone.Context)\n */\nexport function getGlobalAudioContext(): AudioContext {\n return getGlobalContext().rawContext as AudioContext;\n}\n\n/**\n * @deprecated Use getGlobalContext() instead\n * Get the Tone.js Context's rawContext typed as IAudioContext\n * @returns The rawContext cast as IAudioContext\n */\nexport function getGlobalToneContext(): Context {\n return getGlobalContext();\n}\n\n/**\n * Resume the global AudioContext if it's suspended\n * Should be called in response to a user gesture (e.g., button click)\n * @returns Promise that resolves when context is running\n */\nexport async function resumeGlobalAudioContext(): Promise<void> {\n const context = getGlobalContext();\n if (context.state !== 'running') {\n await context.resume();\n }\n}\n\n/**\n * Get the current state of the global AudioContext\n * @returns The AudioContext state ('suspended', 'running', or 'closed')\n */\nexport function getGlobalAudioContextState(): AudioContextState {\n return globalToneContext?.rawContext.state || 'suspended';\n}\n\n/**\n * Close the global AudioContext\n * Should only be called when the application is shutting down\n */\nexport async function closeGlobalAudioContext(): Promise<void> {\n if (globalToneContext && globalToneContext.rawContext.state !== 'closed') {\n await globalToneContext.close();\n globalToneContext = null;\n }\n}\n","/**\n * MediaStreamSource Manager\n *\n * Manages MediaStreamAudioSourceNode instances to ensure only one source\n * is created per MediaStream per AudioContext.\n *\n * Web Audio API constraint: You can only create one MediaStreamAudioSourceNode\n * per MediaStream per AudioContext. Multiple attempts will fail or disconnect\n * previous sources.\n *\n * This manager ensures a single source is shared across multiple consumers\n * (e.g., AnalyserNode for VU meter, AudioWorkletNode for recording).\n *\n * NOTE: With Tone.js Context, you can also use context.createMediaStreamSource()\n * directly, which handles cross-browser compatibility internally.\n */\n\nimport { getContext } from 'tone';\n\n// Map of MediaStream -> MediaStreamAudioSourceNode\nconst streamSources = new Map<MediaStream, MediaStreamAudioSourceNode>();\n\n// Map of MediaStream -> cleanup handlers\nconst streamCleanupHandlers = new Map<MediaStream, () => void>();\n\n/**\n * Get or create a MediaStreamAudioSourceNode for the given stream\n *\n * @param stream - The MediaStream to create a source for\n * @returns MediaStreamAudioSourceNode that can be connected to multiple nodes\n *\n * @example\n * ```typescript\n * const source = getMediaStreamSource(stream);\n *\n * // Multiple consumers can connect to the same source\n * source.connect(analyserNode); // For VU meter\n * source.connect(workletNode); // For recording\n * ```\n */\nexport function getMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode {\n // Return existing source if we have one for this stream\n if (streamSources.has(stream)) {\n return streamSources.get(stream)!;\n }\n\n // Create new source using Tone.js's shared context for cross-browser compatibility\n const context = getContext();\n const source = context.createMediaStreamSource(stream);\n streamSources.set(stream, source);\n\n // Set up cleanup when stream ends\n const cleanup = () => {\n source.disconnect();\n streamSources.delete(stream);\n streamCleanupHandlers.delete(stream);\n\n // Remove event listener\n stream.removeEventListener('ended', cleanup);\n stream.removeEventListener('inactive', cleanup);\n };\n\n streamCleanupHandlers.set(stream, cleanup);\n\n // Clean up when stream ends or becomes inactive\n stream.addEventListener('ended', cleanup);\n stream.addEventListener('inactive', cleanup);\n\n return source;\n}\n\n/**\n * Manually release a MediaStreamSource\n *\n * Normally you don't need to call this - cleanup happens automatically\n * when the stream ends. Only call this if you need to force cleanup.\n *\n * @param stream - The MediaStream to release the source for\n */\nexport function releaseMediaStreamSource(stream: MediaStream): void {\n const cleanup = streamCleanupHandlers.get(stream);\n if (cleanup) {\n cleanup();\n }\n}\n\n/**\n * Check if a MediaStreamSource exists for the given stream\n *\n * @param stream - The MediaStream to check\n * @returns true if a source exists for this stream\n */\nexport function hasMediaStreamSource(stream: MediaStream): boolean {\n return streamSources.has(stream);\n}\n","import type { ClipTrack, Track } from '@waveform-playlist/core';\nimport {\n clipStartTime,\n clipEndTime,\n clipOffsetTime,\n clipDurationTime,\n} from '@waveform-playlist/core';\nimport type { PlayoutAdapter } from '@waveform-playlist/engine';\nimport { TonePlayout } from './TonePlayout';\nimport type { EffectsFunction } from './TonePlayout';\nimport type { ClipInfo } from './ToneTrack';\nimport type { MidiClipInfo } from './MidiToneTrack';\nimport type { SoundFontCache } from './SoundFontCache';\nimport { now } from 'tone';\n\nexport interface ToneAdapterOptions {\n effects?: EffectsFunction;\n /** When provided, MIDI clips use SoundFont sample playback instead of PolySynth */\n soundFontCache?: SoundFontCache;\n}\n\nexport function createToneAdapter(options?: ToneAdapterOptions): PlayoutAdapter {\n let playout: TonePlayout | null = null;\n let _isPlaying = false;\n let _playoutGeneration = 0;\n let _loopEnabled = false;\n let _loopStart = 0;\n let _loopEnd = 0;\n let _audioInitialized = false;\n\n // Add a single ClipTrack to the playout (shared by buildPlayout and addTrack)\n function addTrackToPlayout(p: TonePlayout, track: ClipTrack): void {\n const audioClips = track.clips.filter((c) => c.audioBuffer && !c.midiNotes);\n const midiClips = track.clips.filter((c) => c.midiNotes && c.midiNotes.length > 0);\n\n if (audioClips.length > 0) {\n const startTime = Math.min(...audioClips.map(clipStartTime));\n const endTime = Math.max(...audioClips.map(clipEndTime));\n\n const trackObj: Track = {\n id: track.id,\n name: track.name,\n gain: track.volume,\n muted: track.muted,\n soloed: track.soloed,\n stereoPan: track.pan,\n startTime,\n endTime,\n };\n\n const clipInfos: ClipInfo[] = audioClips.map((clip) => ({\n buffer: clip.audioBuffer!,\n startTime: clipStartTime(clip) - startTime,\n duration: clipDurationTime(clip),\n offset: clipOffsetTime(clip),\n fadeIn: clip.fadeIn,\n fadeOut: clip.fadeOut,\n gain: clip.gain,\n }));\n\n p.addTrack({\n clips: clipInfos,\n track: trackObj,\n effects: track.effects,\n });\n }\n\n if (midiClips.length > 0) {\n const startTime = Math.min(...midiClips.map(clipStartTime));\n const endTime = Math.max(...midiClips.map(clipEndTime));\n\n const trackId = audioClips.length > 0 ? `${track.id}:midi` : track.id;\n\n const trackObj: Track = {\n id: trackId,\n name: track.name,\n gain: track.volume,\n muted: track.muted,\n soloed: track.soloed,\n stereoPan: track.pan,\n startTime,\n endTime,\n };\n\n const midiClipInfos: MidiClipInfo[] = midiClips.map((clip) => ({\n notes: clip.midiNotes!,\n startTime: clipStartTime(clip) - startTime,\n duration: clipDurationTime(clip),\n offset: clipOffsetTime(clip),\n }));\n\n if (options?.soundFontCache?.isLoaded) {\n const firstClip = midiClips[0];\n const midiChannel = firstClip.midiChannel;\n const isPercussion = midiChannel === 9;\n const programNumber = firstClip.midiProgram ?? 0;\n\n p.addSoundFontTrack({\n clips: midiClipInfos,\n track: trackObj,\n soundFontCache: options.soundFontCache,\n programNumber,\n isPercussion,\n effects: track.effects,\n });\n } else {\n if (options?.soundFontCache) {\n console.warn(\n `[waveform-playlist] SoundFont not loaded for track \"${track.name}\" — falling back to PolySynth.`\n );\n }\n p.addMidiTrack({\n clips: midiClipInfos,\n track: trackObj,\n effects: track.effects,\n });\n }\n }\n }\n\n function buildPlayout(tracks: ClipTrack[]): void {\n if (playout) {\n try {\n playout.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing previous playout during rebuild:', err);\n }\n playout = null;\n }\n\n _playoutGeneration++;\n const generation = _playoutGeneration;\n\n playout = new TonePlayout({\n effects: options?.effects,\n });\n\n // If Tone.start() was already called (AudioContext resumed), carry\n // initialization forward. Tone.start() is safe to call multiple times —\n // it resolves immediately if the AudioContext is already running.\n if (_audioInitialized) {\n playout.init().catch((err) => {\n console.warn(\n '[waveform-playlist] Failed to re-initialize playout after rebuild. ' +\n 'Audio playback will require another user gesture.',\n err\n );\n _audioInitialized = false;\n });\n }\n\n for (const track of tracks) {\n addTrackToPlayout(playout, track);\n }\n playout.applyInitialSoloState();\n playout.setLoop(_loopEnabled, _loopStart, _loopEnd);\n\n playout.setOnPlaybackComplete(() => {\n if (generation === _playoutGeneration) {\n _isPlaying = false;\n }\n });\n }\n\n return {\n async init(): Promise<void> {\n if (playout) {\n await playout.init();\n _audioInitialized = true;\n }\n },\n\n setTracks(tracks: ClipTrack[]): void {\n buildPlayout(tracks);\n },\n\n addTrack(track: ClipTrack): void {\n if (!playout) {\n throw new Error(\n '[waveform-playlist] adapter.addTrack() called but no playout exists. ' +\n 'Call setTracks() first to initialize the playout.'\n );\n }\n addTrackToPlayout(playout, track);\n playout.applyInitialSoloState();\n },\n\n removeTrack(trackId: string): void {\n if (!playout) return;\n playout.removeTrack(trackId);\n playout.applyInitialSoloState();\n },\n\n play(startTime: number, endTime?: number): void {\n if (!playout) {\n console.warn(\n '[waveform-playlist] adapter.play() called but no playout is available. ' +\n 'Tracks may not have been set, or the adapter was disposed.'\n );\n return;\n }\n const duration = endTime !== undefined ? endTime - startTime : undefined;\n playout.play(now(), startTime, duration);\n // Only set _isPlaying if play() didn't throw\n // (TonePlayout.play() re-throws after cleanup on Transport failure)\n _isPlaying = true;\n },\n\n pause(): void {\n playout?.pause();\n _isPlaying = false;\n },\n\n stop(): void {\n playout?.stop();\n _isPlaying = false;\n },\n\n seek(time: number): void {\n playout?.seekTo(time);\n },\n\n getCurrentTime(): number {\n return playout?.getCurrentTime() ?? 0;\n },\n\n isPlaying(): boolean {\n return _isPlaying;\n },\n\n setMasterVolume(volume: number): void {\n playout?.setMasterGain(volume);\n },\n\n setTrackVolume(trackId: string, volume: number): void {\n playout?.getTrack(trackId)?.setVolume(volume);\n },\n\n setTrackMute(trackId: string, muted: boolean): void {\n playout?.setMute(trackId, muted);\n },\n\n setTrackSolo(trackId: string, soloed: boolean): void {\n playout?.setSolo(trackId, soloed);\n },\n\n setTrackPan(trackId: string, pan: number): void {\n playout?.getTrack(trackId)?.setPan(pan);\n },\n\n setLoop(enabled: boolean, start: number, end: number): void {\n _loopEnabled = enabled;\n _loopStart = start;\n _loopEnd = end;\n playout?.setLoop(enabled, start, end);\n },\n\n dispose(): void {\n try {\n playout?.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing playout:', err);\n }\n playout = null;\n _isPlaying = false;\n },\n };\n}\n"],"mappings":"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","names":["import_tone","clipStartTime","import_tone","now","import_tone","import_tone","import_tone","import_core","import_tone","start"]}
package/dist/index.mjs CHANGED
@@ -1590,6 +1590,11 @@ function createToneAdapter(options) {
1590
1590
  addTrackToPlayout(playout, track);
1591
1591
  playout.applyInitialSoloState();
1592
1592
  },
1593
+ removeTrack(trackId) {
1594
+ if (!playout) return;
1595
+ playout.removeTrack(trackId);
1596
+ playout.applyInitialSoloState();
1597
+ },
1593
1598
  play(startTime, endTime) {
1594
1599
  if (!playout) {
1595
1600
  console.warn(
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/TonePlayout.ts","../src/ToneTrack.ts","../src/fades.ts","../src/MidiToneTrack.ts","../src/SoundFontToneTrack.ts","../src/SoundFontCache.ts","../src/audioContext.ts","../src/mediaStreamSourceManager.ts","../src/TonePlayoutAdapter.ts"],"sourcesContent":["import {\n Volume,\n ToneAudioNode,\n getDestination,\n start,\n now,\n getTransport,\n getContext,\n BaseContext,\n} from 'tone';\nimport { ToneTrack, ToneTrackOptions } from './ToneTrack';\nimport { MidiToneTrack, MidiToneTrackOptions } from './MidiToneTrack';\nimport type { PlayableTrack } from './MidiToneTrack';\nimport { SoundFontToneTrack, SoundFontToneTrackOptions } from './SoundFontToneTrack';\n\nexport type EffectsFunction = (\n masterGainNode: Volume,\n destination: ToneAudioNode,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface TonePlayoutOptions {\n tracks?: ToneTrack[];\n masterGain?: number;\n effects?: EffectsFunction;\n}\n\nexport class TonePlayout {\n private tracks: Map<string, PlayableTrack> = new Map();\n private masterVolume: Volume;\n private isInitialized = false;\n private soloedTracks: Set<string> = new Set();\n private manualMuteState: Map<string, boolean> = new Map();\n private effectsCleanup?: () => void;\n private onPlaybackCompleteCallback?: () => void;\n private _completionEventId: number | null = null;\n private _loopHandler: (() => void) | null = null;\n private _loopEnabled = false;\n private _loopStart = 0;\n private _loopEnd = 0;\n\n constructor(options: TonePlayoutOptions = {}) {\n this.masterVolume = new Volume(this.gainToDb(options.masterGain ?? 1));\n\n // Setup effects chain if provided, otherwise connect directly to destination\n if (options.effects) {\n const cleanup = options.effects(this.masterVolume, getDestination(), false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.masterVolume.toDestination();\n }\n\n if (options.tracks) {\n options.tracks.forEach((track) => {\n this.tracks.set(track.id, track);\n this.manualMuteState.set(track.id, track.muted);\n });\n }\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n private clearCompletionEvent(): void {\n if (this._completionEventId !== null) {\n try {\n getTransport().clear(this._completionEventId);\n } catch (err) {\n console.warn('[waveform-playlist] Error clearing Transport completion event:', err);\n }\n this._completionEventId = null;\n }\n }\n\n async init(): Promise<void> {\n if (this.isInitialized) return;\n\n await start();\n this.isInitialized = true;\n }\n\n addTrack(trackOptions: ToneTrackOptions): ToneTrack {\n // Ensure tracks connect to master volume instead of destination\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const toneTrack = new ToneTrack(optionsWithDestination);\n this.tracks.set(toneTrack.id, toneTrack);\n this.manualMuteState.set(toneTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(toneTrack.id);\n }\n return toneTrack;\n }\n\n addMidiTrack(trackOptions: MidiToneTrackOptions): MidiToneTrack {\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const midiTrack = new MidiToneTrack(optionsWithDestination);\n this.tracks.set(midiTrack.id, midiTrack);\n this.manualMuteState.set(midiTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(midiTrack.id);\n }\n return midiTrack;\n }\n\n addSoundFontTrack(trackOptions: SoundFontToneTrackOptions): SoundFontToneTrack {\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const sfTrack = new SoundFontToneTrack(optionsWithDestination);\n this.tracks.set(sfTrack.id, sfTrack);\n this.manualMuteState.set(sfTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(sfTrack.id);\n }\n return sfTrack;\n }\n\n /**\n * Apply solo muting after all tracks have been added.\n * Call this after adding all tracks to ensure solo logic is applied correctly.\n */\n applyInitialSoloState(): void {\n this.updateSoloMuting();\n }\n\n removeTrack(trackId: string): void {\n const track = this.tracks.get(trackId);\n if (track) {\n track.dispose();\n this.tracks.delete(trackId);\n this.manualMuteState.delete(trackId);\n this.soloedTracks.delete(trackId);\n }\n }\n\n getTrack(trackId: string): PlayableTrack | undefined {\n return this.tracks.get(trackId);\n }\n\n play(when?: number, offset?: number, duration?: number): void {\n if (!this.isInitialized) {\n throw new Error('[waveform-playlist] TonePlayout not initialized. Call init() first.');\n }\n\n const startTime = when ?? now();\n const transport = getTransport();\n\n this.clearCompletionEvent();\n\n const transportOffset = offset ?? 0;\n this.tracks.forEach((track) => {\n track.cancelFades();\n track.prepareFades(startTime, transportOffset);\n });\n\n // Schedule duration-limited stop via Transport\n if (duration !== undefined) {\n this._completionEventId = transport.scheduleOnce(() => {\n this._completionEventId = null;\n try {\n this.onPlaybackCompleteCallback?.();\n } catch (err) {\n console.warn('[waveform-playlist] Error in playback completion callback:', err);\n }\n }, transportOffset + duration);\n }\n\n // Start Transport — triggers schedule() callbacks for clips at/after offset\n try {\n // Stop all active native sources before restarting to prevent layered audio.\n // Native AudioBufferSourceNodes don't respond to Transport state changes.\n if (transport.state !== 'stopped') {\n transport.stop();\n }\n this.tracks.forEach((track) => track.stopAllSources());\n\n // Set loop boundaries BEFORE enabling loop. _processTick checks\n // `ticks >= _loopEnd` every tick; _loopEnd defaults to 0.\n transport.loopStart = this._loopStart;\n transport.loopEnd = this._loopEnd;\n transport.loop = this._loopEnabled;\n\n // Set schedule guard BEFORE transport.start(). Ghost ticks from stale\n // Clock._lastUpdate can fire schedule callbacks at past positions;\n // the guard suppresses callbacks for clips before the play offset\n // (those are handled by startMidClipSources below).\n this.tracks.forEach((track) => track.setScheduleGuardOffset(transportOffset));\n\n if (offset !== undefined) {\n transport.start(startTime, offset);\n } else {\n transport.start(startTime);\n }\n\n // Advance Clock._lastUpdate past the stop/start boundary.\n //\n // After stop/start cycles, _lastUpdate is stale (set by the previous\n // context tick, before our stop/start). The next Clock._loop() processes\n // ticks from [_lastUpdate, now()]. In the gap [_lastUpdate, startTime),\n // the TickSource is still \"started\" from the PREVIOUS play cycle with\n // its old tick offset. Those accumulated ticks can exceed _loopEnd,\n // causing _processTick to wrap immediately to loopStart.\n //\n // By advancing _lastUpdate to startTime, we skip the stale range.\n // The next Clock._loop() only processes [startTime, now()] — ticks\n // from the current play cycle with the correct offset.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tone.js private internal, see CLAUDE.md ghost tick fix\n (transport as any)._clock._lastUpdate = startTime;\n\n // Start sources for clips that span the current Transport position.\n // Transport.schedule() only fires for clips at/after the offset;\n // clips whose start time is before the offset need manual creation.\n this.tracks.forEach((track) => {\n track.startMidClipSources(transportOffset, startTime);\n });\n } catch (err) {\n // Clean up scheduled events since Transport failed to start\n this.clearCompletionEvent();\n this.tracks.forEach((track) => track.cancelFades());\n console.warn(\n '[waveform-playlist] Transport.start() failed. Audio playback could not begin.',\n err\n );\n throw err;\n }\n }\n\n pause(): void {\n const transport = getTransport();\n try {\n transport.pause();\n } catch (err) {\n console.warn('[waveform-playlist] Transport.pause() failed:', err);\n }\n // Native AudioBufferSourceNodes ignore Transport state changes —\n // they must be explicitly stopped.\n this.tracks.forEach((track) => track.stopAllSources());\n this.tracks.forEach((track) => track.cancelFades());\n this.clearCompletionEvent();\n }\n\n stop(): void {\n const transport = getTransport();\n try {\n transport.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Transport.stop() failed:', err);\n }\n // Remove loop handler before stopping sources. Prevents any deferred\n // loop event from creating new sources via startMidClipSources() after\n // stopAllSources() runs. Defense-in-depth — setLoop(false) should\n // already have removed it, but stop() must be self-contained.\n if (this._loopHandler) {\n try {\n transport.off('loop', this._loopHandler);\n } catch (err) {\n console.warn('[waveform-playlist] Error removing loop handler:', err);\n }\n this._loopHandler = null;\n }\n // Stop all native sources explicitly\n this.tracks.forEach((track) => {\n try {\n track.stopAllSources();\n } catch (err) {\n console.warn(`[waveform-playlist] Error stopping sources for track \"${track.id}\":`, err);\n }\n });\n this.tracks.forEach((track) => {\n try {\n track.cancelFades();\n } catch (err) {\n console.warn(`[waveform-playlist] Error canceling fades for track \"${track.id}\":`, err);\n }\n });\n this.clearCompletionEvent();\n }\n\n setMasterGain(gain: number): void {\n this.masterVolume.volume.value = this.gainToDb(gain);\n }\n\n setSolo(trackId: string, soloed: boolean): void {\n const track = this.tracks.get(trackId);\n if (track) {\n track.setSolo(soloed);\n if (soloed) {\n this.soloedTracks.add(trackId);\n } else {\n this.soloedTracks.delete(trackId);\n }\n this.updateSoloMuting();\n }\n }\n\n private updateSoloMuting(): void {\n const hasSoloedTracks = this.soloedTracks.size > 0;\n\n this.tracks.forEach((track, id) => {\n if (hasSoloedTracks) {\n if (!this.soloedTracks.has(id)) {\n track.setMute(true);\n } else {\n const manuallyMuted = this.manualMuteState.get(id) ?? false;\n track.setMute(manuallyMuted);\n }\n } else {\n const manuallyMuted = this.manualMuteState.get(id) ?? false;\n track.setMute(manuallyMuted);\n }\n });\n }\n\n setMute(trackId: string, muted: boolean): void {\n const track = this.tracks.get(trackId);\n if (track) {\n this.manualMuteState.set(trackId, muted);\n track.setMute(muted);\n }\n }\n\n setLoop(enabled: boolean, loopStart: number, loopEnd: number): void {\n // Update cached state first — play() uses these values to configure\n // the Transport on next start, so they must reflect the caller's intent\n // regardless of whether Transport property setting succeeds.\n this._loopEnabled = enabled;\n this._loopStart = loopStart;\n this._loopEnd = loopEnd;\n\n const transport = getTransport();\n try {\n // Set boundaries BEFORE enabling loop. Tone.js's _processTick checks\n // `ticks >= _loopEnd` on every tick. If we set transport.loop = true\n // first, a tick could fire before loopEnd is updated, seeing the stale\n // _loopEnd value (0 from Transport default) and wrapping immediately.\n transport.loopStart = loopStart;\n transport.loopEnd = loopEnd;\n transport.loop = enabled;\n } catch (err) {\n console.warn('[waveform-playlist] Error configuring Transport loop:', err);\n return;\n }\n\n if (enabled && !this._loopHandler) {\n this._loopHandler = () => {\n // On loop boundary: stop old sources, re-schedule fades, start mid-clip sources.\n // Event ordering in Transport's tick processing (Tone.js 15.x _processTick):\n // loopEnd → ticks reset → loopStart → loop → forEachAtTime(ticks)\n // Our loop handler fires BEFORE schedule callbacks, so:\n // 1. stopAllSources + cancelFades — clean slate\n // 2. startMidClipSources — for clips spanning loopStart boundary\n // 3. prepareFades — fresh fade envelopes\n // Then Transport fires schedule callbacks for clips at/after loopStart.\n const currentTime = now();\n this.tracks.forEach((track) => {\n try {\n track.stopAllSources();\n track.cancelFades();\n track.setScheduleGuardOffset(this._loopStart);\n track.startMidClipSources(this._loopStart, currentTime);\n track.prepareFades(currentTime, this._loopStart);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error re-scheduling track \"${track.id}\" on loop:`,\n err\n );\n }\n });\n };\n transport.on('loop', this._loopHandler);\n } else if (!enabled && this._loopHandler) {\n transport.off('loop', this._loopHandler);\n this._loopHandler = null;\n }\n }\n\n getCurrentTime(): number {\n return getTransport().seconds;\n }\n\n seekTo(time: number): void {\n getTransport().seconds = time;\n }\n\n dispose(): void {\n // Stop Transport and all active sources before disposing.\n // Without this, the global Transport singleton keeps firing scheduled\n // callbacks and native AudioBufferSourceNodes continue playing through\n // the shared AudioContext (e.g., during Docusaurus client-side navigation).\n try {\n this.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Error stopping Transport during dispose:', err);\n }\n\n this.tracks.forEach((track) => {\n try {\n track.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing track \"${track.id}\":`, err);\n }\n });\n this.tracks.clear();\n\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn('[waveform-playlist] Error during master effects cleanup:', err);\n }\n }\n\n try {\n this.masterVolume.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing master volume:', err);\n }\n }\n\n get context(): BaseContext {\n return getContext();\n }\n\n get sampleRate(): number {\n return getContext().sampleRate;\n }\n\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n}\n","import {\n Volume,\n Gain,\n Panner,\n ToneAudioNode,\n getDestination,\n getTransport,\n getContext,\n} from 'tone';\nimport { Track, type Fade } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, getUnderlyingAudioParam } from './fades';\n\nexport type TrackEffectsFunction = (\n graphEnd: Gain,\n masterGainNode: ToneAudioNode,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface ClipInfo {\n buffer: AudioBuffer;\n startTime: number; // When this clip starts in the track timeline (seconds)\n duration: number; // How long this clip plays (seconds)\n offset: number; // Where to start playing within the buffer (seconds)\n fadeIn?: Fade;\n fadeOut?: Fade;\n gain: number; // Clip-level gain\n}\n\nexport interface ToneTrackOptions {\n buffer?: AudioBuffer; // Legacy: single buffer (deprecated, use clips instead)\n clips?: ClipInfo[]; // Modern: array of clips\n track: Track;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n}\n\n/** Per-clip scheduling info and audio nodes */\ninterface ScheduledClip {\n clipInfo: ClipInfo;\n fadeGainNode: GainNode; // Native GainNode for per-clip fade envelope\n scheduleId: number; // Transport.schedule() event ID\n}\n\nexport class ToneTrack {\n private scheduledClips: ScheduledClip[];\n private activeSources: Set<AudioBufferSourceNode> = new Set();\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n // Guard against ghost tick schedule callbacks. After stop/start cycles with\n // loops, stale Clock._lastUpdate causes ticks from the previous cycle to fire\n // Transport.schedule() callbacks at past positions (e.g., time 0 clips fire\n // when starting at offset 5s). Clips before this offset are handled by\n // startMidClipSources(); schedule callbacks should only create sources for\n // clips at/after this offset.\n private _scheduleGuardOffset = 0;\n\n constructor(options: ToneTrackOptions) {\n this.track = options.track;\n\n // Create shared track-level Tone.js nodes\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n // Tone.js Panner defaults to channelCount: 1 + channelCountMode: 'explicit',\n // which forces stereo→mono downmix (1/√2 attenuation) before panning.\n // Override to channelCount: 2 to preserve stereo recordings.\n this.panNode = new Panner({ pan: options.track.stereoPan, channelCount: 2 });\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n\n // Chain shared Tone.js nodes: Volume → Pan → MuteGain\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create clips array - support both legacy single buffer and modern clips array\n const clipInfos: ClipInfo[] =\n options.clips ||\n (options.buffer\n ? [\n {\n buffer: options.buffer,\n startTime: 0,\n duration: options.buffer.duration,\n offset: 0,\n fadeIn: options.track.fadeIn,\n fadeOut: options.track.fadeOut,\n gain: 1,\n },\n ]\n : []);\n\n const transport = getTransport();\n const rawContext = getContext().rawContext as AudioContext;\n\n // Get the native AudioNode input of the Volume for native→Tone connection.\n // Volume.input is a Tone.js Gain<\"decibels\"> whose .input is the native GainNode.\n // Cast through unknown since Gain<\"decibels\"> and Gain<\"gain\"> don't overlap.\n const volumeNativeInput = (this.volumeNode.input as unknown as Gain).input;\n\n // Schedule each clip via Transport.schedule() with native AudioBufferSourceNode\n this.scheduledClips = clipInfos.map((clipInfo) => {\n // Native GainNode for per-clip fade envelope — created once, reused across play cycles\n const fadeGainNode = rawContext.createGain();\n fadeGainNode.gain.value = clipInfo.gain;\n fadeGainNode.connect(volumeNativeInput);\n\n // Schedule a permanent Transport event at the clip's absolute timeline position.\n // This callback fires on every play and every loop iteration when Transport\n // passes this point.\n const absTransportTime = this.track.startTime + clipInfo.startTime;\n const scheduleId = transport.schedule((audioContextTime: number) => {\n // Guard: ghost ticks from stale Clock._lastUpdate can fire this callback\n // at past positions (see Tone.js #1419). Clips before the play/loop offset\n // are already handled by startMidClipSources().\n if (absTransportTime < this._scheduleGuardOffset) {\n return;\n }\n this.startClipSource(clipInfo, fadeGainNode, audioContextTime);\n }, absTransportTime);\n\n return { clipInfo, fadeGainNode, scheduleId };\n });\n }\n\n /**\n * Create and start an AudioBufferSourceNode for a clip.\n * Sources are one-shot: each play or loop iteration creates a fresh one.\n */\n private startClipSource(\n clipInfo: ClipInfo,\n fadeGainNode: GainNode,\n audioContextTime: number,\n bufferOffset?: number,\n playDuration?: number\n ): void {\n const rawContext = getContext().rawContext as AudioContext;\n const source = rawContext.createBufferSource();\n source.buffer = clipInfo.buffer;\n source.connect(fadeGainNode);\n\n const offset = bufferOffset ?? clipInfo.offset;\n const duration = playDuration ?? clipInfo.duration;\n\n try {\n source.start(audioContextTime, offset, duration);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start source on track \"${this.id}\" ` +\n `(time=${audioContextTime}, offset=${offset}, duration=${duration}):`,\n err\n );\n source.disconnect();\n return;\n }\n\n this.activeSources.add(source);\n source.onended = () => {\n this.activeSources.delete(source);\n };\n }\n\n /**\n * Set the schedule guard offset. Schedule callbacks for clips before this\n * offset are suppressed (already handled by startMidClipSources).\n * Must be called before transport.start() and in the loop handler.\n */\n setScheduleGuardOffset(offset: number): void {\n this._scheduleGuardOffset = offset;\n }\n\n /**\n * Start sources for clips that span the given Transport position.\n * Used for mid-playback seeking and loop boundary handling where\n * Transport.schedule() callbacks have already passed.\n *\n * Uses strict < for absClipStart to avoid double-creation with\n * schedule callbacks at exact Transport position (e.g., loopStart).\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo, fadeGainNode } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n // Only handle clips that started before the transport position\n // but haven't ended yet (i.e., the transport is \"inside\" the clip)\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n const elapsed = transportOffset - absClipStart;\n const adjustedOffset = clipInfo.offset + elapsed;\n const remainingDuration = clipInfo.duration - elapsed;\n this.startClipSource(\n clipInfo,\n fadeGainNode,\n audioContextTime,\n adjustedOffset,\n remainingDuration\n );\n }\n }\n }\n\n /**\n * Stop all active AudioBufferSourceNodes and clear the set.\n * Native AudioBufferSourceNodes ignore Transport state changes —\n * they must be explicitly stopped.\n */\n stopAllSources(): void {\n this.activeSources.forEach((source) => {\n try {\n source.stop();\n } catch (err) {\n console.warn(`[waveform-playlist] Error stopping source on track \"${this.id}\":`, err);\n }\n });\n this.activeSources.clear();\n }\n\n /**\n * Schedule fade envelopes for a clip at the given AudioContext time.\n * Uses native GainNode.gain (AudioParam) directly — no _param workaround needed.\n */\n private scheduleFades(\n scheduled: ScheduledClip,\n clipStartTime: number,\n clipOffset: number = 0\n ): void {\n const { clipInfo, fadeGainNode } = scheduled;\n const audioParam = fadeGainNode.gain;\n\n // Cancel any previous automation\n audioParam.cancelScheduledValues(0);\n\n // Calculate how much of the clip we're skipping (for seeking)\n const skipTime = clipOffset - clipInfo.offset;\n\n // Apply fade in if it exists and we haven't skipped past it\n if (clipInfo.fadeIn && skipTime < clipInfo.fadeIn.duration) {\n const fadeInDuration = clipInfo.fadeIn.duration;\n\n if (skipTime <= 0) {\n applyFadeIn(\n audioParam,\n clipStartTime,\n fadeInDuration,\n clipInfo.fadeIn.type || 'linear',\n 0,\n clipInfo.gain\n );\n } else {\n const remainingFadeDuration = fadeInDuration - skipTime;\n const fadeProgress = skipTime / fadeInDuration;\n const startValue = clipInfo.gain * fadeProgress;\n applyFadeIn(\n audioParam,\n clipStartTime,\n remainingFadeDuration,\n clipInfo.fadeIn.type || 'linear',\n startValue,\n clipInfo.gain\n );\n }\n } else {\n audioParam.setValueAtTime(clipInfo.gain, clipStartTime);\n }\n\n // Apply fade out if it exists\n if (clipInfo.fadeOut) {\n const fadeOutStart = clipInfo.duration - clipInfo.fadeOut.duration;\n const fadeOutStartInClip = fadeOutStart - skipTime;\n\n if (fadeOutStartInClip > 0) {\n const absoluteFadeOutStart = clipStartTime + fadeOutStartInClip;\n applyFadeOut(\n audioParam,\n absoluteFadeOutStart,\n clipInfo.fadeOut.duration,\n clipInfo.fadeOut.type || 'linear',\n clipInfo.gain,\n 0\n );\n } else if (fadeOutStartInClip > -clipInfo.fadeOut.duration) {\n const elapsedFadeOut = -fadeOutStartInClip;\n const remainingFadeDuration = clipInfo.fadeOut.duration - elapsedFadeOut;\n const fadeProgress = elapsedFadeOut / clipInfo.fadeOut.duration;\n const startValue = clipInfo.gain * (1 - fadeProgress);\n applyFadeOut(\n audioParam,\n clipStartTime,\n remainingFadeDuration,\n clipInfo.fadeOut.type || 'linear',\n startValue,\n 0\n );\n }\n }\n }\n\n /**\n * Prepare fade envelopes for all clips based on Transport offset.\n * Called before Transport.start() to schedule fades at correct AudioContext times.\n */\n prepareFades(when: number, transportOffset: number): void {\n this.scheduledClips.forEach((scheduled) => {\n const absClipStart = this.track.startTime + scheduled.clipInfo.startTime;\n const absClipEnd = absClipStart + scheduled.clipInfo.duration;\n\n if (transportOffset >= absClipEnd) return; // clip already finished\n\n if (transportOffset >= absClipStart) {\n // Mid-clip: playing now\n const clipOffset = transportOffset - absClipStart + scheduled.clipInfo.offset;\n this.scheduleFades(scheduled, when, clipOffset);\n } else {\n // Clip starts later\n const delay = absClipStart - transportOffset;\n this.scheduleFades(scheduled, when + delay, scheduled.clipInfo.offset);\n }\n });\n }\n\n /**\n * Cancel all scheduled fade automation and reset to nominal gain.\n * Called on pause/stop to prevent stale fade envelopes.\n */\n cancelFades(): void {\n this.scheduledClips.forEach(({ fadeGainNode, clipInfo }) => {\n const audioParam = fadeGainNode.gain;\n audioParam.cancelScheduledValues(0);\n audioParam.setValueAtTime(clipInfo.gain, 0);\n });\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n // Use setValueAtTime on the raw AudioParam to ensure the value is applied\n // even when the AudioContext is suspended. Setting .gain.value on the Tone.js\n // Signal wrapper doesn't propagate to the underlying AudioParam until the\n // context resumes, causing a brief audio glitch (e.g., all tracks audible\n // before solo muting takes effect).\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n const transport = getTransport();\n\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(`[waveform-playlist] Error during track \"${this.id}\" effects cleanup:`, err);\n }\n }\n\n this.stopAllSources();\n\n // Clear Transport schedule events and disconnect native fade gain nodes\n this.scheduledClips.forEach((scheduled, index) => {\n try {\n transport.clear(scheduled.scheduleId);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error clearing schedule ${index} on track \"${this.id}\":`,\n err\n );\n }\n try {\n scheduled.fadeGainNode.disconnect();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disconnecting fadeGain ${index} on track \"${this.id}\":`,\n err\n );\n }\n });\n\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing volumeNode on track \"${this.id}\":`, err);\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing panNode on track \"${this.id}\":`, err);\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing muteGain on track \"${this.id}\":`, err);\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get buffer(): AudioBuffer {\n return this.scheduledClips[0]?.clipInfo.buffer;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","/**\n * Fade utilities — re-exports from @waveform-playlist/core\n * plus Tone.js-specific helpers\n */\n\n// Re-export all pure fade utilities from core\nexport {\n linearCurve,\n exponentialCurve,\n sCurveCurve,\n logarithmicCurve,\n generateCurve,\n applyFadeIn,\n applyFadeOut,\n} from '@waveform-playlist/core';\n\nexport type { FadeType, FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Access the underlying Web Audio AudioParam from a Tone.js Signal/Param wrapper.\n *\n * Tone.js wraps native AudioParam in its Signal class, but sometimes we need\n * direct access to the raw AudioParam for setValueAtTime/cancelScheduledValues\n * (e.g., when the AudioContext is suspended and Tone.js Signal doesn't propagate).\n *\n * This uses `_param` which is a private Tone.js 15.x internal.\n * Pin the Tone.js version carefully if upgrading.\n *\n * @param signal - A Tone.js Signal or Param wrapper (e.g., `gain.gain`)\n * @returns The underlying AudioParam, or undefined if not found\n */\nlet hasWarned = false;\n\nexport function getUnderlyingAudioParam(signal: unknown): AudioParam | undefined {\n const param = (signal as { _param?: AudioParam })._param;\n if (!param && !hasWarned) {\n hasWarned = true;\n console.warn(\n '[waveform-playlist] Unable to access Tone.js internal _param. ' +\n 'This likely means the Tone.js version is incompatible. ' +\n 'Mute scheduling may not work correctly.'\n );\n }\n return param;\n}\n","import {\n Volume,\n Gain,\n Panner,\n PolySynth,\n Synth,\n MembraneSynth,\n MetalSynth,\n NoiseSynth,\n Part,\n ToneAudioNode,\n getDestination,\n getContext,\n} from 'tone';\nimport type { SynthOptions } from 'tone';\nimport { Track } from '@waveform-playlist/core';\nimport type { MidiNoteData } from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam } from './fades';\nimport type { TrackEffectsFunction } from './ToneTrack';\n\n/**\n * Shared interface for tracks managed by TonePlayout.\n * Both ToneTrack (audio) and MidiToneTrack (MIDI) implement this,\n * allowing TonePlayout to manage them uniformly.\n */\nexport interface PlayableTrack {\n id: string;\n startTime: number;\n muted: boolean;\n duration: number;\n stopAllSources(): void;\n startMidClipSources(offset: number, time: number): void;\n setScheduleGuardOffset(offset: number): void;\n prepareFades(when: number, offset: number): void;\n cancelFades(): void;\n setVolume(gain: number): void;\n setPan(pan: number): void;\n setMute(muted: boolean): void;\n setSolo(soloed: boolean): void;\n dispose(): void;\n}\n\nexport interface MidiClipInfo {\n notes: MidiNoteData[];\n startTime: number; // When this clip starts relative to track start (seconds)\n duration: number; // Clip duration (seconds)\n offset: number; // Trim offset into the MIDI data (seconds)\n}\n\nexport interface MidiToneTrackOptions {\n clips: MidiClipInfo[];\n track: Track;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n synthOptions?: Partial<SynthOptions>;\n}\n\n/**\n * Categorize GM percussion note numbers into synth types.\n * See: https://www.midi.org/specifications-old/item/gm-level-1-sound-set\n */\ntype DrumCategory = 'kick' | 'snare' | 'cymbal' | 'tom';\n\nfunction getDrumCategory(midiNote: number): DrumCategory {\n // Bass drums\n if (midiNote === 35 || midiNote === 36) return 'kick';\n // Snare, side stick, clap\n if (midiNote >= 37 && midiNote <= 40) return 'snare';\n // Toms (low floor tom through high tom)\n if (\n midiNote === 41 ||\n midiNote === 43 ||\n midiNote === 45 ||\n midiNote === 47 ||\n midiNote === 48 ||\n midiNote === 50\n )\n return 'tom';\n // Hi-hats, cymbals, bells, tambourine, cowbell, etc.\n return 'cymbal';\n}\n\n/** Per-clip scheduling info */\ninterface ScheduledMidiClip {\n clipInfo: MidiClipInfo;\n part: Part;\n}\n\n/**\n * MIDI track that always creates both melodic and percussion synths.\n * Per-note routing uses the `channel` field on each MidiNoteData:\n * channel 9 → percussion synths, all others → melodic PolySynth.\n * This enables flattened tracks (mixed channels) to play correctly.\n */\nexport class MidiToneTrack implements PlayableTrack {\n private scheduledClips: ScheduledMidiClip[];\n // Melodic synth — always created\n private synth: PolySynth;\n // Percussion synths — always created (PolySynth wrappers for polyphony)\n private kickSynth: PolySynth<MembraneSynth>;\n private snareSynth: NoiseSynth; // No pitch param, can't wrap in PolySynth\n private cymbalSynth: PolySynth<MetalSynth>;\n private tomSynth: PolySynth<MembraneSynth>;\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n\n constructor(options: MidiToneTrackOptions) {\n this.track = options.track;\n\n // Create shared track-level Tone.js nodes (same chain as ToneTrack)\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n this.panNode = new Panner(options.track.stereoPan);\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Melodic: PolySynth with basic Synth voice\n this.synth = new PolySynth(Synth, options.synthOptions);\n this.synth.connect(this.volumeNode);\n\n // Percussion: PolySynth wrappers for polyphonic playback\n this.kickSynth = new PolySynth(MembraneSynth, {\n voice: MembraneSynth,\n options: {\n pitchDecay: 0.05,\n octaves: 6,\n envelope: { attack: 0.001, decay: 0.4, sustain: 0, release: 0.1 },\n },\n } as never);\n this.snareSynth = new NoiseSynth({\n noise: { type: 'white' },\n envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.05 },\n });\n this.cymbalSynth = new PolySynth(MetalSynth, {\n voice: MetalSynth,\n options: {\n envelope: { attack: 0.001, decay: 0.3, release: 0.1 },\n harmonicity: 5.1,\n modulationIndex: 32,\n resonance: 4000,\n octaves: 1.5,\n },\n } as never);\n this.tomSynth = new PolySynth(MembraneSynth, {\n voice: MembraneSynth,\n options: {\n pitchDecay: 0.08,\n octaves: 4,\n envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.1 },\n },\n } as never);\n\n this.kickSynth.connect(this.volumeNode);\n this.snareSynth.connect(this.volumeNode);\n this.cymbalSynth.connect(this.volumeNode);\n this.tomSynth.connect(this.volumeNode);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create a Tone.Part for each clip, scheduling notes relative to Transport\n this.scheduledClips = options.clips.map((clipInfo) => {\n // Filter notes within the clip's visible window (after trim offset)\n const visibleNotes = clipInfo.notes.filter((note) => {\n const noteEnd = note.time + note.duration;\n // Note must start before clip ends and end after clip's trim offset\n return note.time < clipInfo.offset + clipInfo.duration && noteEnd > clipInfo.offset;\n });\n\n // Create Part events with absolute Transport times\n const absClipStart = this.track.startTime + clipInfo.startTime;\n\n const partEvents = visibleNotes.map((note) => {\n // Adjust note timing relative to clip's trim offset\n const adjustedTime = note.time - clipInfo.offset;\n // Clamp to clip boundaries\n const clampedStart = Math.max(0, adjustedTime);\n const clampedDuration = Math.min(\n note.duration - Math.max(0, clipInfo.offset - note.time),\n clipInfo.duration - clampedStart\n );\n\n return {\n time: absClipStart + clampedStart,\n note: note.name,\n midi: note.midi,\n duration: Math.max(0, clampedDuration),\n velocity: note.velocity,\n channel: note.channel,\n };\n });\n\n const part = new Part((time, event) => {\n if (event.duration > 0) {\n this.triggerNote(\n event.midi,\n event.note,\n event.duration,\n time,\n event.velocity,\n event.channel\n );\n }\n }, partEvents);\n\n // Part starts automatically with Transport\n part.start(0);\n\n return { clipInfo, part };\n });\n }\n\n /**\n * Trigger a note using the appropriate synth.\n * Routes per-note: channel 9 → percussion synths, others → melodic PolySynth.\n */\n private triggerNote(\n midiNote: number,\n noteName: string,\n duration: number,\n time: number,\n velocity: number,\n channel?: number\n ): void {\n if (channel === 9) {\n const category = getDrumCategory(midiNote);\n switch (category) {\n case 'kick':\n this.kickSynth.triggerAttackRelease('C1', duration, time, velocity);\n break;\n case 'snare':\n // NoiseSynth is monophonic — wrap in try-catch for rare overlaps\n try {\n this.snareSynth.triggerAttackRelease(duration, time, velocity);\n } catch (err) {\n console.warn(\n '[waveform-playlist] Snare overlap — previous hit still decaying, skipped:',\n err\n );\n }\n break;\n case 'tom': {\n const tomPitches: Record<number, string> = {\n 41: 'G1',\n 43: 'A1',\n 45: 'C2',\n 47: 'D2',\n 48: 'E2',\n 50: 'G2',\n };\n this.tomSynth.triggerAttackRelease(\n tomPitches[midiNote] || 'C2',\n duration,\n time,\n velocity\n );\n break;\n }\n case 'cymbal':\n // PolySynth requires a note arg; MetalSynth uses it as fundamental frequency.\n // 'C4' provides a reasonable metallic timbre for all cymbal/hi-hat hits.\n this.cymbalSynth.triggerAttackRelease('C4', duration, time, velocity);\n break;\n }\n } else {\n this.synth.triggerAttackRelease(noteName, duration, time, velocity);\n }\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n /**\n * No-op for MIDI — schedule guard is for AudioBufferSourceNode ghost tick prevention.\n * Tone.Part handles its own scheduling relative to Transport.\n */\n setScheduleGuardOffset(_offset: number): void {\n // No-op: Tone.Part handles scheduling internally\n }\n\n /**\n * For MIDI, mid-clip sources are notes that should already be sounding.\n * We trigger them with their remaining duration.\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n // Find notes that should be currently sounding\n for (const note of clipInfo.notes) {\n const adjustedTime = note.time - clipInfo.offset;\n const noteAbsStart = absClipStart + Math.max(0, adjustedTime);\n const noteAbsEnd = noteAbsStart + note.duration;\n\n if (noteAbsStart < transportOffset && noteAbsEnd > transportOffset) {\n const remainingDuration = noteAbsEnd - transportOffset;\n try {\n this.triggerNote(\n note.midi,\n note.name,\n remainingDuration,\n audioContextTime,\n note.velocity,\n note.channel\n );\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start mid-clip MIDI note \"${note.name}\" on track \"${this.id}\":`,\n err\n );\n }\n }\n }\n }\n }\n }\n\n /**\n * Stop all sounding notes and cancel scheduled Part events.\n */\n stopAllSources(): void {\n const now = getContext().rawContext.currentTime;\n try {\n this.synth.releaseAll(now);\n this.kickSynth.releaseAll(now);\n this.cymbalSynth.releaseAll(now);\n this.tomSynth.releaseAll(now);\n // NoiseSynth has no releaseAll — it decays naturally via short envelope\n } catch (err) {\n console.warn(`[waveform-playlist] Error releasing synth on track \"${this.id}\":`, err);\n }\n }\n\n /**\n * No-op for MIDI — MIDI uses note velocity, not gain fades.\n */\n prepareFades(_when: number, _offset: number): void {\n // No-op: MIDI clips don't use fade envelopes\n }\n\n /**\n * No-op for MIDI — no fade automation to cancel.\n */\n cancelFades(): void {\n // No-op: MIDI clips don't use fade envelopes\n }\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error during MIDI track \"${this.id}\" effects cleanup:`,\n err\n );\n }\n }\n\n this.stopAllSources();\n\n // Dispose Parts\n this.scheduledClips.forEach(({ part }, index) => {\n try {\n part.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing Part ${index} on MIDI track \"${this.id}\":`,\n err\n );\n }\n });\n\n // Dispose all synths\n const synthsToDispose = [\n this.synth,\n this.kickSynth,\n this.snareSynth,\n this.cymbalSynth,\n this.tomSynth,\n ];\n for (const s of synthsToDispose) {\n try {\n s?.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing synth on MIDI track \"${this.id}\":`, err);\n }\n }\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing volumeNode on MIDI track \"${this.id}\":`,\n err\n );\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing panNode on MIDI track \"${this.id}\":`, err);\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing muteGain on MIDI track \"${this.id}\":`, err);\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","import { Volume, Gain, Panner, Part, ToneAudioNode, getDestination, getContext } from 'tone';\nimport { Track } from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam } from './fades';\nimport type { TrackEffectsFunction } from './ToneTrack';\nimport type { PlayableTrack, MidiClipInfo } from './MidiToneTrack';\nimport type { SoundFontCache } from './SoundFontCache';\n\nexport interface SoundFontToneTrackOptions {\n clips: MidiClipInfo[];\n track: Track;\n soundFontCache: SoundFontCache;\n /** GM program number (0-127) for melodic instruments */\n programNumber?: number;\n /** Whether this track uses percussion bank (channel 9) */\n isPercussion?: boolean;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n}\n\n/** Per-clip scheduling info */\ninterface ScheduledMidiClip {\n clipInfo: MidiClipInfo;\n part: Part;\n}\n\n/**\n * MIDI track that uses SoundFont samples for playback.\n *\n * Instead of PolySynth synthesis, each note triggers the correct instrument\n * sample from an SF2 file, pitch-shifted via AudioBufferSourceNode.playbackRate.\n *\n * Audio graph per note:\n * AudioBufferSourceNode (native, one-shot, pitch-shifted)\n * → GainNode (native, per-note velocity)\n * → Volume.input (Tone.js, shared per-track)\n * → Panner → muteGain → effects/destination\n */\nexport class SoundFontToneTrack implements PlayableTrack {\n /** Rate-limit missing sample warnings — one per class lifetime */\n private static _missingSampleWarned = false;\n private scheduledClips: ScheduledMidiClip[];\n private activeSources: Set<AudioBufferSourceNode> = new Set();\n private soundFontCache: SoundFontCache;\n private programNumber: number;\n private bankNumber: number;\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n\n constructor(options: SoundFontToneTrackOptions) {\n this.track = options.track;\n this.soundFontCache = options.soundFontCache;\n this.programNumber = options.programNumber ?? 0;\n // Bank 128 for percussion (channel 9), bank 0 for melodic\n this.bankNumber = options.isPercussion ? 128 : 0;\n\n // Create shared track-level Tone.js nodes (same chain as ToneTrack)\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n this.panNode = new Panner(options.track.stereoPan);\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create a Tone.Part for each clip, scheduling notes relative to Transport\n this.scheduledClips = options.clips.map((clipInfo) => {\n // Filter notes within the clip's visible window (after trim offset)\n const visibleNotes = clipInfo.notes.filter((note) => {\n const noteEnd = note.time + note.duration;\n return note.time < clipInfo.offset + clipInfo.duration && noteEnd > clipInfo.offset;\n });\n\n const absClipStart = this.track.startTime + clipInfo.startTime;\n\n const partEvents = visibleNotes.map((note) => {\n const adjustedTime = note.time - clipInfo.offset;\n const clampedStart = Math.max(0, adjustedTime);\n const clampedDuration = Math.min(\n note.duration - Math.max(0, clipInfo.offset - note.time),\n clipInfo.duration - clampedStart\n );\n\n return {\n time: absClipStart + clampedStart,\n note: note.name,\n midi: note.midi,\n duration: Math.max(0, clampedDuration),\n velocity: note.velocity,\n channel: note.channel,\n };\n });\n\n const part = new Part((time, event) => {\n if (event.duration > 0) {\n this.triggerNote(event.midi, event.duration, time, event.velocity, event.channel);\n }\n }, partEvents);\n\n part.start(0);\n\n return { clipInfo, part };\n });\n }\n\n /**\n * Trigger a note by creating a native AudioBufferSourceNode from the SoundFont cache.\n *\n * Per-note routing: channel 9 → bank 128 (drums), others → bank 0 with programNumber.\n */\n private triggerNote(\n midiNote: number,\n duration: number,\n time: number,\n velocity: number,\n channel?: number\n ): void {\n const bank = channel === 9 ? 128 : this.bankNumber;\n const preset = channel === 9 ? 0 : this.programNumber;\n\n const sfSample = this.soundFontCache.getAudioBuffer(midiNote, bank, preset);\n if (!sfSample) {\n if (!SoundFontToneTrack._missingSampleWarned) {\n console.warn(\n `[waveform-playlist] SoundFont sample not found for MIDI note ${midiNote} (bank ${bank}, preset ${preset}). ` +\n 'Subsequent missing samples will be silent.'\n );\n SoundFontToneTrack._missingSampleWarned = true;\n }\n return;\n }\n\n const rawContext = getContext().rawContext as BaseAudioContext;\n\n // Create AudioBufferSourceNode with optional looping\n const source = rawContext.createBufferSource();\n source.buffer = sfSample.buffer;\n source.playbackRate.value = sfSample.playbackRate;\n\n // Enable sample looping if SF2 zone defines loop mode\n if (sfSample.loopMode === 1 || sfSample.loopMode === 3) {\n source.loop = true;\n source.loopStart = sfSample.loopStart;\n source.loopEnd = sfSample.loopEnd;\n }\n\n // For non-looping samples (percussion, one-shots), use the sample's natural\n // buffer duration so cymbals/drums ring out fully. For looping samples\n // (sustained instruments), use the MIDI note duration for note-off timing.\n const sampleDuration = sfSample.buffer.duration / sfSample.playbackRate;\n const effectiveDuration =\n sfSample.loopMode === 0 ? Math.max(duration, sampleDuration) : duration;\n\n // Per-note gain with SF2 volume ADSR envelope\n const peakGain = velocity * velocity;\n const gainNode = rawContext.createGain();\n const { attackVolEnv, holdVolEnv, decayVolEnv, sustainVolEnv, releaseVolEnv } = sfSample;\n const sustainGain = peakGain * sustainVolEnv;\n\n // Attack: start silent, ramp to peak\n gainNode.gain.setValueAtTime(0, time);\n gainNode.gain.linearRampToValueAtTime(peakGain, time + attackVolEnv);\n // Hold at peak\n if (holdVolEnv > 0.001) {\n gainNode.gain.setValueAtTime(peakGain, time + attackVolEnv + holdVolEnv);\n }\n // Decay to sustain level\n const decayStart = time + attackVolEnv + holdVolEnv;\n gainNode.gain.linearRampToValueAtTime(sustainGain, decayStart + decayVolEnv);\n // Sustain holds until note-off, then release ramps to silence.\n // For short notes (duration < AHD), setValueAtTime at note-off cancels the\n // incomplete decay ramp — Web Audio handles this correctly.\n gainNode.gain.setValueAtTime(sustainGain, time + effectiveDuration);\n gainNode.gain.linearRampToValueAtTime(0, time + effectiveDuration + releaseVolEnv);\n\n // Connect: source → gainNode → Volume.input (Tone.js)\n source.connect(gainNode);\n gainNode.connect((this.volumeNode.input as unknown as Gain).input);\n\n // Track active sources for stopAllSources()\n this.activeSources.add(source);\n source.onended = () => {\n this.activeSources.delete(source);\n try {\n gainNode.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] GainNode already disconnected:', err);\n }\n };\n\n source.start(time);\n source.stop(time + effectiveDuration + releaseVolEnv);\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n /**\n * No-op — Tone.Part handles scheduling internally, no ghost tick guard needed.\n */\n setScheduleGuardOffset(_offset: number): void {\n // No-op\n }\n\n /**\n * Start notes that should already be sounding at the current transport offset.\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n for (const note of clipInfo.notes) {\n const adjustedTime = note.time - clipInfo.offset;\n const noteAbsStart = absClipStart + Math.max(0, adjustedTime);\n const noteAbsEnd = noteAbsStart + note.duration;\n\n if (noteAbsStart < transportOffset && noteAbsEnd > transportOffset) {\n const remainingDuration = noteAbsEnd - transportOffset;\n try {\n this.triggerNote(\n note.midi,\n remainingDuration,\n audioContextTime,\n note.velocity,\n note.channel\n );\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start mid-clip SoundFont note on track \"${this.id}\":`,\n err\n );\n }\n }\n }\n }\n }\n }\n\n /**\n * Stop all active AudioBufferSourceNodes.\n */\n stopAllSources(): void {\n for (const source of this.activeSources) {\n try {\n source.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Error stopping AudioBufferSourceNode:', err);\n }\n }\n this.activeSources.clear();\n }\n\n /** No-op for MIDI — MIDI uses note velocity, not gain fades. */\n prepareFades(_when: number, _offset: number): void {}\n\n /** No-op for MIDI — no fade automation to cancel. */\n cancelFades(): void {}\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error during SoundFont track \"${this.id}\" effects cleanup:`,\n err\n );\n }\n }\n\n this.stopAllSources();\n\n // Dispose Parts\n this.scheduledClips.forEach(({ part }, index) => {\n try {\n part.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing Part ${index} on SoundFont track \"${this.id}\":`,\n err\n );\n }\n });\n\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing volumeNode on SoundFont track \"${this.id}\":`,\n err\n );\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing panNode on SoundFont track \"${this.id}\":`,\n err\n );\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing muteGain on SoundFont track \"${this.id}\":`,\n err\n );\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","import { SoundFont2, GeneratorType } from 'soundfont2';\nimport type { Generator, ZoneMap } from 'soundfont2';\n\n/**\n * Result of looking up a MIDI note in the SoundFont.\n * Contains the AudioBuffer, playbackRate, loop points, and volume envelope.\n */\nexport interface SoundFontSample {\n /** Cached AudioBuffer for this sample */\n buffer: AudioBuffer;\n /** Playback rate to pitch-shift from originalPitch to target note */\n playbackRate: number;\n /** Loop mode: 0=no loop, 1=continuous, 3=sustain loop */\n loopMode: number;\n /** Loop start in seconds, relative to AudioBuffer start */\n loopStart: number;\n /** Loop end in seconds, relative to AudioBuffer start */\n loopEnd: number;\n /** Volume envelope attack time in seconds */\n attackVolEnv: number;\n /** Volume envelope hold time in seconds */\n holdVolEnv: number;\n /** Volume envelope decay time in seconds */\n decayVolEnv: number;\n /** Volume envelope sustain level as linear gain 0-1 */\n sustainVolEnv: number;\n /** Volume envelope release time in seconds */\n releaseVolEnv: number;\n}\n\n/**\n * Convert SF2 timecents to seconds.\n * SF2 formula: seconds = 2^(timecents / 1200)\n * Default -12000 timecents ≈ 0.001s (effectively instant).\n */\nexport function timecentsToSeconds(tc: number): number {\n return Math.pow(2, tc / 1200);\n}\n\n/** Max release time to prevent extremely long tails from stale generators */\nconst MAX_RELEASE_SECONDS = 5;\n\n/**\n * Get a numeric generator value from a zone map.\n */\nexport function getGeneratorValue(\n generators: ZoneMap<Generator>,\n type: GeneratorType\n): number | undefined {\n return generators[type]?.value;\n}\n\n/**\n * Convert Int16Array sample data to Float32Array.\n * SF2 samples are 16-bit signed integers; Web Audio needs Float32 [-1, 1].\n */\nexport function int16ToFloat32(samples: Int16Array): Float32Array {\n const floats = new Float32Array(samples.length);\n for (let i = 0; i < samples.length; i++) {\n floats[i] = samples[i] / 32768;\n }\n return floats;\n}\n\n/**\n * Input parameters for playback rate calculation.\n */\nexport interface PlaybackRateParams {\n /** Target MIDI note number (0-127) */\n midiNote: number;\n /** OverridingRootKey generator value, or undefined if not set */\n overrideRootKey: number | undefined;\n /** sample.header.originalPitch (255 means unpitched) */\n originalPitch: number;\n /** CoarseTune generator value in semitones (default 0) */\n coarseTune: number;\n /** FineTune generator value in cents (default 0) */\n fineTune: number;\n /** sample.header.pitchCorrection in cents (default 0) */\n pitchCorrection: number;\n}\n\n/**\n * Calculate playback rate for a MIDI note using the SF2 generator chain.\n *\n * SF2 root key resolution priority:\n * 1. OverridingRootKey generator (per-zone, most specific)\n * 2. sample.header.originalPitch (sample header)\n * 3. MIDI note 60 (middle C fallback)\n *\n * Tuning adjustments:\n * - CoarseTune generator (semitones, additive)\n * - FineTune generator (cents, additive)\n * - sample.header.pitchCorrection (cents, additive)\n */\nexport function calculatePlaybackRate(params: PlaybackRateParams): number {\n const { midiNote, overrideRootKey, originalPitch, coarseTune, fineTune, pitchCorrection } =\n params;\n\n // Resolve root key: OverridingRootKey → originalPitch → 60\n const rootKey =\n overrideRootKey !== undefined ? overrideRootKey : originalPitch !== 255 ? originalPitch : 60;\n\n // Total offset in semitones: target note - root key + tuning\n const totalSemitones = midiNote - rootKey + coarseTune + (fineTune + pitchCorrection) / 100;\n\n return Math.pow(2, totalSemitones / 12);\n}\n\n/**\n * Input parameters for loop and envelope extraction.\n */\nexport interface LoopAndEnvelopeParams {\n /** SF2 generators zone map */\n generators: ZoneMap<Generator>;\n /** Sample header with loop points and sample rate */\n header: {\n startLoop: number;\n endLoop: number;\n sampleRate: number;\n };\n}\n\n/**\n * Extract loop points and volume envelope data from per-zone generators.\n *\n * Loop points are stored as absolute indices into the SF2 sample pool.\n * We convert to AudioBuffer-relative seconds by subtracting header.start\n * and dividing by sampleRate.\n *\n * Volume envelope times are in SF2 timecents; sustain is centibels attenuation.\n */\nexport function extractLoopAndEnvelope(\n params: LoopAndEnvelopeParams\n): Omit<SoundFontSample, 'buffer' | 'playbackRate'> {\n const { generators, header } = params;\n\n // --- Loop points ---\n const loopMode = getGeneratorValue(generators, GeneratorType.SampleModes) ?? 0;\n\n // Compute actual loop positions (header + fine/coarse generator offsets)\n const rawLoopStart =\n header.startLoop +\n (getGeneratorValue(generators, GeneratorType.StartLoopAddrsOffset) ?? 0) +\n (getGeneratorValue(generators, GeneratorType.StartLoopAddrsCoarseOffset) ?? 0) * 32768;\n const rawLoopEnd =\n header.endLoop +\n (getGeneratorValue(generators, GeneratorType.EndLoopAddrsOffset) ?? 0) +\n (getGeneratorValue(generators, GeneratorType.EndLoopAddrsCoarseOffset) ?? 0) * 32768;\n\n // The soundfont2 library already converts startLoop/endLoop to be\n // relative to sample.data (subtracts header.start during parsing),\n // so we only need to divide by sampleRate to get seconds.\n const loopStart = rawLoopStart / header.sampleRate;\n const loopEnd = rawLoopEnd / header.sampleRate;\n\n // --- Volume envelope ---\n const attackVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.AttackVolEnv) ?? -12000\n );\n const holdVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.HoldVolEnv) ?? -12000\n );\n const decayVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.DecayVolEnv) ?? -12000\n );\n const releaseVolEnv = Math.min(\n timecentsToSeconds(getGeneratorValue(generators, GeneratorType.ReleaseVolEnv) ?? -12000),\n MAX_RELEASE_SECONDS\n );\n\n // SustainVolEnv is centibels attenuation: 0 = full volume, 1440 = silence\n // Convert to linear gain: 10^(-cb / 200)\n const sustainCb = getGeneratorValue(generators, GeneratorType.SustainVolEnv) ?? 0;\n const sustainVolEnv = Math.pow(10, -sustainCb / 200);\n\n return {\n loopMode,\n loopStart,\n loopEnd,\n attackVolEnv,\n holdVolEnv,\n decayVolEnv,\n sustainVolEnv,\n releaseVolEnv,\n };\n}\n\n/**\n * Caches parsed SoundFont2 data and AudioBuffers for efficient playback.\n *\n * AudioBuffers are created lazily on first access and cached by sample index.\n * Pitch calculation uses the SF2 generator chain:\n * OverridingRootKey → sample.header.originalPitch → fallback 60\n *\n * Audio graph per note:\n * AudioBufferSourceNode (playbackRate for pitch) → GainNode (velocity) → track chain\n */\nexport class SoundFontCache {\n private sf2: SoundFont2 | null = null;\n private audioBufferCache: Map<number, AudioBuffer> = new Map();\n private context: BaseAudioContext;\n\n /**\n * @param context Optional AudioContext for createBuffer(). If omitted, uses\n * an OfflineAudioContext which doesn't require user gesture — safe to\n * construct before user interaction (avoids Firefox autoplay warnings).\n */\n constructor(context?: BaseAudioContext) {\n // OfflineAudioContext only needs valid params; we never call startRendering().\n // It's used solely for createBuffer() which works identically to AudioContext.\n this.context = context ?? new OfflineAudioContext(1, 1, 44100);\n }\n\n /**\n * Load and parse an SF2 file from a URL.\n */\n async load(url: string, signal?: AbortSignal): Promise<void> {\n const response = await fetch(url, { signal });\n if (!response.ok) {\n throw new Error(`Failed to fetch SoundFont ${url}: ${response.statusText}`);\n }\n const arrayBuffer = await response.arrayBuffer();\n try {\n this.sf2 = new SoundFont2(new Uint8Array(arrayBuffer));\n } catch (err) {\n throw new Error(\n `Failed to parse SoundFont ${url}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n /**\n * Load from an already-fetched ArrayBuffer.\n */\n loadFromBuffer(data: ArrayBuffer): void {\n try {\n this.sf2 = new SoundFont2(new Uint8Array(data));\n } catch (err) {\n throw new Error(\n `Failed to parse SoundFont from buffer: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n get isLoaded(): boolean {\n return this.sf2 !== null;\n }\n\n /**\n * Look up a MIDI note and return the AudioBuffer + playbackRate.\n *\n * @param midiNote - MIDI note number (0-127)\n * @param bankNumber - Bank number (0 for melodic, 128 for percussion/drums)\n * @param presetNumber - GM program number (0-127)\n * @returns SoundFontSample or null if no sample found for this note\n */\n getAudioBuffer(\n midiNote: number,\n bankNumber: number = 0,\n presetNumber: number = 0\n ): SoundFontSample | null {\n if (!this.sf2) return null;\n\n const keyData = this.sf2.getKeyData(midiNote, bankNumber, presetNumber);\n if (!keyData) return null;\n\n const sample = keyData.sample;\n const sampleIndex = this.sf2.samples.indexOf(sample);\n\n // Get or create the AudioBuffer for this sample\n let buffer = this.audioBufferCache.get(sampleIndex);\n if (!buffer) {\n buffer = this.int16ToAudioBuffer(sample.data, sample.header.sampleRate);\n this.audioBufferCache.set(sampleIndex, buffer);\n }\n\n // Calculate playback rate using SF2 generator chain for root key.\n // Priority: OverridingRootKey generator → sample.header.originalPitch → 60\n const playbackRate = calculatePlaybackRate({\n midiNote,\n overrideRootKey: getGeneratorValue(keyData.generators, GeneratorType.OverridingRootKey),\n originalPitch: sample.header.originalPitch,\n coarseTune: getGeneratorValue(keyData.generators, GeneratorType.CoarseTune) ?? 0,\n fineTune: getGeneratorValue(keyData.generators, GeneratorType.FineTune) ?? 0,\n pitchCorrection: sample.header.pitchCorrection ?? 0,\n });\n\n // Extract per-zone loop points and volume envelope from generators\n const loopAndEnvelope = extractLoopAndEnvelope({\n generators: keyData.generators,\n header: keyData.sample.header,\n });\n\n return { buffer, playbackRate, ...loopAndEnvelope };\n }\n\n /**\n * Convert Int16Array sample data to an AudioBuffer.\n * Uses the extracted int16ToFloat32 for the conversion, then copies into an AudioBuffer.\n */\n private int16ToAudioBuffer(data: Int16Array, sampleRate: number): AudioBuffer {\n const floats = int16ToFloat32(data);\n const buffer = this.context.createBuffer(1, floats.length, sampleRate);\n buffer.getChannelData(0).set(floats);\n return buffer;\n }\n\n /**\n * Clear all cached AudioBuffers and release the parsed SF2.\n */\n dispose(): void {\n this.audioBufferCache.clear();\n this.sf2 = null;\n }\n}\n","/**\n * Global AudioContext Manager\n *\n * Provides a single AudioContext shared across the entire application.\n * This context is used by Tone.js for playback and by all recording/monitoring hooks.\n *\n * Uses Tone.js's Context class which wraps standardized-audio-context for\n * cross-browser compatibility (fixes Firefox AudioListener issues).\n */\n\nimport { Context, setContext } from 'tone';\n\nlet globalToneContext: Context | null = null;\n\n/**\n * Get the global Tone.js Context\n * This is the main context for cross-browser audio operations.\n * Use context.createAudioWorkletNode(), context.createMediaStreamSource(), etc.\n * @returns The Tone.js Context instance\n */\nexport function getGlobalContext(): Context {\n if (!globalToneContext) {\n globalToneContext = new Context();\n setContext(globalToneContext);\n }\n return globalToneContext;\n}\n\n/**\n * Get or create the global AudioContext\n * Uses Tone.js Context for cross-browser compatibility\n * @returns The global AudioContext instance (rawContext from Tone.Context)\n */\nexport function getGlobalAudioContext(): AudioContext {\n return getGlobalContext().rawContext as AudioContext;\n}\n\n/**\n * @deprecated Use getGlobalContext() instead\n * Get the Tone.js Context's rawContext typed as IAudioContext\n * @returns The rawContext cast as IAudioContext\n */\nexport function getGlobalToneContext(): Context {\n return getGlobalContext();\n}\n\n/**\n * Resume the global AudioContext if it's suspended\n * Should be called in response to a user gesture (e.g., button click)\n * @returns Promise that resolves when context is running\n */\nexport async function resumeGlobalAudioContext(): Promise<void> {\n const context = getGlobalContext();\n if (context.state !== 'running') {\n await context.resume();\n }\n}\n\n/**\n * Get the current state of the global AudioContext\n * @returns The AudioContext state ('suspended', 'running', or 'closed')\n */\nexport function getGlobalAudioContextState(): AudioContextState {\n return globalToneContext?.rawContext.state || 'suspended';\n}\n\n/**\n * Close the global AudioContext\n * Should only be called when the application is shutting down\n */\nexport async function closeGlobalAudioContext(): Promise<void> {\n if (globalToneContext && globalToneContext.rawContext.state !== 'closed') {\n await globalToneContext.close();\n globalToneContext = null;\n }\n}\n","/**\n * MediaStreamSource Manager\n *\n * Manages MediaStreamAudioSourceNode instances to ensure only one source\n * is created per MediaStream per AudioContext.\n *\n * Web Audio API constraint: You can only create one MediaStreamAudioSourceNode\n * per MediaStream per AudioContext. Multiple attempts will fail or disconnect\n * previous sources.\n *\n * This manager ensures a single source is shared across multiple consumers\n * (e.g., AnalyserNode for VU meter, AudioWorkletNode for recording).\n *\n * NOTE: With Tone.js Context, you can also use context.createMediaStreamSource()\n * directly, which handles cross-browser compatibility internally.\n */\n\nimport { getContext } from 'tone';\n\n// Map of MediaStream -> MediaStreamAudioSourceNode\nconst streamSources = new Map<MediaStream, MediaStreamAudioSourceNode>();\n\n// Map of MediaStream -> cleanup handlers\nconst streamCleanupHandlers = new Map<MediaStream, () => void>();\n\n/**\n * Get or create a MediaStreamAudioSourceNode for the given stream\n *\n * @param stream - The MediaStream to create a source for\n * @returns MediaStreamAudioSourceNode that can be connected to multiple nodes\n *\n * @example\n * ```typescript\n * const source = getMediaStreamSource(stream);\n *\n * // Multiple consumers can connect to the same source\n * source.connect(analyserNode); // For VU meter\n * source.connect(workletNode); // For recording\n * ```\n */\nexport function getMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode {\n // Return existing source if we have one for this stream\n if (streamSources.has(stream)) {\n return streamSources.get(stream)!;\n }\n\n // Create new source using Tone.js's shared context for cross-browser compatibility\n const context = getContext();\n const source = context.createMediaStreamSource(stream);\n streamSources.set(stream, source);\n\n // Set up cleanup when stream ends\n const cleanup = () => {\n source.disconnect();\n streamSources.delete(stream);\n streamCleanupHandlers.delete(stream);\n\n // Remove event listener\n stream.removeEventListener('ended', cleanup);\n stream.removeEventListener('inactive', cleanup);\n };\n\n streamCleanupHandlers.set(stream, cleanup);\n\n // Clean up when stream ends or becomes inactive\n stream.addEventListener('ended', cleanup);\n stream.addEventListener('inactive', cleanup);\n\n return source;\n}\n\n/**\n * Manually release a MediaStreamSource\n *\n * Normally you don't need to call this - cleanup happens automatically\n * when the stream ends. Only call this if you need to force cleanup.\n *\n * @param stream - The MediaStream to release the source for\n */\nexport function releaseMediaStreamSource(stream: MediaStream): void {\n const cleanup = streamCleanupHandlers.get(stream);\n if (cleanup) {\n cleanup();\n }\n}\n\n/**\n * Check if a MediaStreamSource exists for the given stream\n *\n * @param stream - The MediaStream to check\n * @returns true if a source exists for this stream\n */\nexport function hasMediaStreamSource(stream: MediaStream): boolean {\n return streamSources.has(stream);\n}\n","import type { ClipTrack, Track } from '@waveform-playlist/core';\nimport {\n clipStartTime,\n clipEndTime,\n clipOffsetTime,\n clipDurationTime,\n} from '@waveform-playlist/core';\nimport type { PlayoutAdapter } from '@waveform-playlist/engine';\nimport { TonePlayout } from './TonePlayout';\nimport type { EffectsFunction } from './TonePlayout';\nimport type { ClipInfo } from './ToneTrack';\nimport type { MidiClipInfo } from './MidiToneTrack';\nimport type { SoundFontCache } from './SoundFontCache';\nimport { now } from 'tone';\n\nexport interface ToneAdapterOptions {\n effects?: EffectsFunction;\n /** When provided, MIDI clips use SoundFont sample playback instead of PolySynth */\n soundFontCache?: SoundFontCache;\n}\n\nexport function createToneAdapter(options?: ToneAdapterOptions): PlayoutAdapter {\n let playout: TonePlayout | null = null;\n let _isPlaying = false;\n let _playoutGeneration = 0;\n let _loopEnabled = false;\n let _loopStart = 0;\n let _loopEnd = 0;\n let _audioInitialized = false;\n\n // Add a single ClipTrack to the playout (shared by buildPlayout and addTrack)\n function addTrackToPlayout(p: TonePlayout, track: ClipTrack): void {\n const audioClips = track.clips.filter((c) => c.audioBuffer && !c.midiNotes);\n const midiClips = track.clips.filter((c) => c.midiNotes && c.midiNotes.length > 0);\n\n if (audioClips.length > 0) {\n const startTime = Math.min(...audioClips.map(clipStartTime));\n const endTime = Math.max(...audioClips.map(clipEndTime));\n\n const trackObj: Track = {\n id: track.id,\n name: track.name,\n gain: track.volume,\n muted: track.muted,\n soloed: track.soloed,\n stereoPan: track.pan,\n startTime,\n endTime,\n };\n\n const clipInfos: ClipInfo[] = audioClips.map((clip) => ({\n buffer: clip.audioBuffer!,\n startTime: clipStartTime(clip) - startTime,\n duration: clipDurationTime(clip),\n offset: clipOffsetTime(clip),\n fadeIn: clip.fadeIn,\n fadeOut: clip.fadeOut,\n gain: clip.gain,\n }));\n\n p.addTrack({\n clips: clipInfos,\n track: trackObj,\n effects: track.effects,\n });\n }\n\n if (midiClips.length > 0) {\n const startTime = Math.min(...midiClips.map(clipStartTime));\n const endTime = Math.max(...midiClips.map(clipEndTime));\n\n const trackId = audioClips.length > 0 ? `${track.id}:midi` : track.id;\n\n const trackObj: Track = {\n id: trackId,\n name: track.name,\n gain: track.volume,\n muted: track.muted,\n soloed: track.soloed,\n stereoPan: track.pan,\n startTime,\n endTime,\n };\n\n const midiClipInfos: MidiClipInfo[] = midiClips.map((clip) => ({\n notes: clip.midiNotes!,\n startTime: clipStartTime(clip) - startTime,\n duration: clipDurationTime(clip),\n offset: clipOffsetTime(clip),\n }));\n\n if (options?.soundFontCache?.isLoaded) {\n const firstClip = midiClips[0];\n const midiChannel = firstClip.midiChannel;\n const isPercussion = midiChannel === 9;\n const programNumber = firstClip.midiProgram ?? 0;\n\n p.addSoundFontTrack({\n clips: midiClipInfos,\n track: trackObj,\n soundFontCache: options.soundFontCache,\n programNumber,\n isPercussion,\n effects: track.effects,\n });\n } else {\n if (options?.soundFontCache) {\n console.warn(\n `[waveform-playlist] SoundFont not loaded for track \"${track.name}\" — falling back to PolySynth.`\n );\n }\n p.addMidiTrack({\n clips: midiClipInfos,\n track: trackObj,\n effects: track.effects,\n });\n }\n }\n }\n\n function buildPlayout(tracks: ClipTrack[]): void {\n if (playout) {\n try {\n playout.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing previous playout during rebuild:', err);\n }\n playout = null;\n }\n\n _playoutGeneration++;\n const generation = _playoutGeneration;\n\n playout = new TonePlayout({\n effects: options?.effects,\n });\n\n // If Tone.start() was already called (AudioContext resumed), carry\n // initialization forward. Tone.start() is safe to call multiple times —\n // it resolves immediately if the AudioContext is already running.\n if (_audioInitialized) {\n playout.init().catch((err) => {\n console.warn(\n '[waveform-playlist] Failed to re-initialize playout after rebuild. ' +\n 'Audio playback will require another user gesture.',\n err\n );\n _audioInitialized = false;\n });\n }\n\n for (const track of tracks) {\n addTrackToPlayout(playout, track);\n }\n playout.applyInitialSoloState();\n playout.setLoop(_loopEnabled, _loopStart, _loopEnd);\n\n playout.setOnPlaybackComplete(() => {\n if (generation === _playoutGeneration) {\n _isPlaying = false;\n }\n });\n }\n\n return {\n async init(): Promise<void> {\n if (playout) {\n await playout.init();\n _audioInitialized = true;\n }\n },\n\n setTracks(tracks: ClipTrack[]): void {\n buildPlayout(tracks);\n },\n\n addTrack(track: ClipTrack): void {\n if (!playout) {\n throw new Error(\n '[waveform-playlist] adapter.addTrack() called but no playout exists. ' +\n 'Call setTracks() first to initialize the playout.'\n );\n }\n addTrackToPlayout(playout, track);\n playout.applyInitialSoloState();\n },\n\n play(startTime: number, endTime?: number): void {\n if (!playout) {\n console.warn(\n '[waveform-playlist] adapter.play() called but no playout is available. ' +\n 'Tracks may not have been set, or the adapter was disposed.'\n );\n return;\n }\n const duration = endTime !== undefined ? endTime - startTime : undefined;\n playout.play(now(), startTime, duration);\n // Only set _isPlaying if play() didn't throw\n // (TonePlayout.play() re-throws after cleanup on Transport failure)\n _isPlaying = true;\n },\n\n pause(): void {\n playout?.pause();\n _isPlaying = false;\n },\n\n stop(): void {\n playout?.stop();\n _isPlaying = false;\n },\n\n seek(time: number): void {\n playout?.seekTo(time);\n },\n\n getCurrentTime(): number {\n return playout?.getCurrentTime() ?? 0;\n },\n\n isPlaying(): boolean {\n return _isPlaying;\n },\n\n setMasterVolume(volume: number): void {\n playout?.setMasterGain(volume);\n },\n\n setTrackVolume(trackId: string, volume: number): void {\n playout?.getTrack(trackId)?.setVolume(volume);\n },\n\n setTrackMute(trackId: string, muted: boolean): void {\n playout?.setMute(trackId, muted);\n },\n\n setTrackSolo(trackId: string, soloed: boolean): void {\n playout?.setSolo(trackId, soloed);\n },\n\n setTrackPan(trackId: string, pan: number): void {\n playout?.getTrack(trackId)?.setPan(pan);\n },\n\n setLoop(enabled: boolean, start: number, end: number): void {\n _loopEnabled = enabled;\n _loopStart = start;\n _loopEnd = end;\n playout?.setLoop(enabled, start, end);\n },\n\n dispose(): void {\n try {\n playout?.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing playout:', err);\n }\n playout = null;\n _isPlaying = false;\n },\n };\n}\n"],"mappings":"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","names":["Volume","getDestination","getTransport","getContext","clipStartTime","Volume","Gain","Panner","getDestination","getContext","Volume","Panner","Gain","getDestination","now","getContext","Volume","Gain","Panner","Part","getDestination","getContext","Volume","Panner","Gain","getDestination","Part","getContext","Volume","getDestination","getTransport","getContext","getContext","now","start"]}
1
+ {"version":3,"sources":["../src/TonePlayout.ts","../src/ToneTrack.ts","../src/fades.ts","../src/MidiToneTrack.ts","../src/SoundFontToneTrack.ts","../src/SoundFontCache.ts","../src/audioContext.ts","../src/mediaStreamSourceManager.ts","../src/TonePlayoutAdapter.ts"],"sourcesContent":["import {\n Volume,\n ToneAudioNode,\n getDestination,\n start,\n now,\n getTransport,\n getContext,\n BaseContext,\n} from 'tone';\nimport { ToneTrack, ToneTrackOptions } from './ToneTrack';\nimport { MidiToneTrack, MidiToneTrackOptions } from './MidiToneTrack';\nimport type { PlayableTrack } from './MidiToneTrack';\nimport { SoundFontToneTrack, SoundFontToneTrackOptions } from './SoundFontToneTrack';\n\nexport type EffectsFunction = (\n masterGainNode: Volume,\n destination: ToneAudioNode,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface TonePlayoutOptions {\n tracks?: ToneTrack[];\n masterGain?: number;\n effects?: EffectsFunction;\n}\n\nexport class TonePlayout {\n private tracks: Map<string, PlayableTrack> = new Map();\n private masterVolume: Volume;\n private isInitialized = false;\n private soloedTracks: Set<string> = new Set();\n private manualMuteState: Map<string, boolean> = new Map();\n private effectsCleanup?: () => void;\n private onPlaybackCompleteCallback?: () => void;\n private _completionEventId: number | null = null;\n private _loopHandler: (() => void) | null = null;\n private _loopEnabled = false;\n private _loopStart = 0;\n private _loopEnd = 0;\n\n constructor(options: TonePlayoutOptions = {}) {\n this.masterVolume = new Volume(this.gainToDb(options.masterGain ?? 1));\n\n // Setup effects chain if provided, otherwise connect directly to destination\n if (options.effects) {\n const cleanup = options.effects(this.masterVolume, getDestination(), false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.masterVolume.toDestination();\n }\n\n if (options.tracks) {\n options.tracks.forEach((track) => {\n this.tracks.set(track.id, track);\n this.manualMuteState.set(track.id, track.muted);\n });\n }\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n private clearCompletionEvent(): void {\n if (this._completionEventId !== null) {\n try {\n getTransport().clear(this._completionEventId);\n } catch (err) {\n console.warn('[waveform-playlist] Error clearing Transport completion event:', err);\n }\n this._completionEventId = null;\n }\n }\n\n async init(): Promise<void> {\n if (this.isInitialized) return;\n\n await start();\n this.isInitialized = true;\n }\n\n addTrack(trackOptions: ToneTrackOptions): ToneTrack {\n // Ensure tracks connect to master volume instead of destination\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const toneTrack = new ToneTrack(optionsWithDestination);\n this.tracks.set(toneTrack.id, toneTrack);\n this.manualMuteState.set(toneTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(toneTrack.id);\n }\n return toneTrack;\n }\n\n addMidiTrack(trackOptions: MidiToneTrackOptions): MidiToneTrack {\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const midiTrack = new MidiToneTrack(optionsWithDestination);\n this.tracks.set(midiTrack.id, midiTrack);\n this.manualMuteState.set(midiTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(midiTrack.id);\n }\n return midiTrack;\n }\n\n addSoundFontTrack(trackOptions: SoundFontToneTrackOptions): SoundFontToneTrack {\n const optionsWithDestination = {\n ...trackOptions,\n destination: this.masterVolume,\n };\n const sfTrack = new SoundFontToneTrack(optionsWithDestination);\n this.tracks.set(sfTrack.id, sfTrack);\n this.manualMuteState.set(sfTrack.id, trackOptions.track.muted ?? false);\n if (trackOptions.track.soloed) {\n this.soloedTracks.add(sfTrack.id);\n }\n return sfTrack;\n }\n\n /**\n * Apply solo muting after all tracks have been added.\n * Call this after adding all tracks to ensure solo logic is applied correctly.\n */\n applyInitialSoloState(): void {\n this.updateSoloMuting();\n }\n\n removeTrack(trackId: string): void {\n const track = this.tracks.get(trackId);\n if (track) {\n track.dispose();\n this.tracks.delete(trackId);\n this.manualMuteState.delete(trackId);\n this.soloedTracks.delete(trackId);\n }\n }\n\n getTrack(trackId: string): PlayableTrack | undefined {\n return this.tracks.get(trackId);\n }\n\n play(when?: number, offset?: number, duration?: number): void {\n if (!this.isInitialized) {\n throw new Error('[waveform-playlist] TonePlayout not initialized. Call init() first.');\n }\n\n const startTime = when ?? now();\n const transport = getTransport();\n\n this.clearCompletionEvent();\n\n const transportOffset = offset ?? 0;\n this.tracks.forEach((track) => {\n track.cancelFades();\n track.prepareFades(startTime, transportOffset);\n });\n\n // Schedule duration-limited stop via Transport\n if (duration !== undefined) {\n this._completionEventId = transport.scheduleOnce(() => {\n this._completionEventId = null;\n try {\n this.onPlaybackCompleteCallback?.();\n } catch (err) {\n console.warn('[waveform-playlist] Error in playback completion callback:', err);\n }\n }, transportOffset + duration);\n }\n\n // Start Transport — triggers schedule() callbacks for clips at/after offset\n try {\n // Stop all active native sources before restarting to prevent layered audio.\n // Native AudioBufferSourceNodes don't respond to Transport state changes.\n if (transport.state !== 'stopped') {\n transport.stop();\n }\n this.tracks.forEach((track) => track.stopAllSources());\n\n // Set loop boundaries BEFORE enabling loop. _processTick checks\n // `ticks >= _loopEnd` every tick; _loopEnd defaults to 0.\n transport.loopStart = this._loopStart;\n transport.loopEnd = this._loopEnd;\n transport.loop = this._loopEnabled;\n\n // Set schedule guard BEFORE transport.start(). Ghost ticks from stale\n // Clock._lastUpdate can fire schedule callbacks at past positions;\n // the guard suppresses callbacks for clips before the play offset\n // (those are handled by startMidClipSources below).\n this.tracks.forEach((track) => track.setScheduleGuardOffset(transportOffset));\n\n if (offset !== undefined) {\n transport.start(startTime, offset);\n } else {\n transport.start(startTime);\n }\n\n // Advance Clock._lastUpdate past the stop/start boundary.\n //\n // After stop/start cycles, _lastUpdate is stale (set by the previous\n // context tick, before our stop/start). The next Clock._loop() processes\n // ticks from [_lastUpdate, now()]. In the gap [_lastUpdate, startTime),\n // the TickSource is still \"started\" from the PREVIOUS play cycle with\n // its old tick offset. Those accumulated ticks can exceed _loopEnd,\n // causing _processTick to wrap immediately to loopStart.\n //\n // By advancing _lastUpdate to startTime, we skip the stale range.\n // The next Clock._loop() only processes [startTime, now()] — ticks\n // from the current play cycle with the correct offset.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tone.js private internal, see CLAUDE.md ghost tick fix\n (transport as any)._clock._lastUpdate = startTime;\n\n // Start sources for clips that span the current Transport position.\n // Transport.schedule() only fires for clips at/after the offset;\n // clips whose start time is before the offset need manual creation.\n this.tracks.forEach((track) => {\n track.startMidClipSources(transportOffset, startTime);\n });\n } catch (err) {\n // Clean up scheduled events since Transport failed to start\n this.clearCompletionEvent();\n this.tracks.forEach((track) => track.cancelFades());\n console.warn(\n '[waveform-playlist] Transport.start() failed. Audio playback could not begin.',\n err\n );\n throw err;\n }\n }\n\n pause(): void {\n const transport = getTransport();\n try {\n transport.pause();\n } catch (err) {\n console.warn('[waveform-playlist] Transport.pause() failed:', err);\n }\n // Native AudioBufferSourceNodes ignore Transport state changes —\n // they must be explicitly stopped.\n this.tracks.forEach((track) => track.stopAllSources());\n this.tracks.forEach((track) => track.cancelFades());\n this.clearCompletionEvent();\n }\n\n stop(): void {\n const transport = getTransport();\n try {\n transport.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Transport.stop() failed:', err);\n }\n // Remove loop handler before stopping sources. Prevents any deferred\n // loop event from creating new sources via startMidClipSources() after\n // stopAllSources() runs. Defense-in-depth — setLoop(false) should\n // already have removed it, but stop() must be self-contained.\n if (this._loopHandler) {\n try {\n transport.off('loop', this._loopHandler);\n } catch (err) {\n console.warn('[waveform-playlist] Error removing loop handler:', err);\n }\n this._loopHandler = null;\n }\n // Stop all native sources explicitly\n this.tracks.forEach((track) => {\n try {\n track.stopAllSources();\n } catch (err) {\n console.warn(`[waveform-playlist] Error stopping sources for track \"${track.id}\":`, err);\n }\n });\n this.tracks.forEach((track) => {\n try {\n track.cancelFades();\n } catch (err) {\n console.warn(`[waveform-playlist] Error canceling fades for track \"${track.id}\":`, err);\n }\n });\n this.clearCompletionEvent();\n }\n\n setMasterGain(gain: number): void {\n this.masterVolume.volume.value = this.gainToDb(gain);\n }\n\n setSolo(trackId: string, soloed: boolean): void {\n const track = this.tracks.get(trackId);\n if (track) {\n track.setSolo(soloed);\n if (soloed) {\n this.soloedTracks.add(trackId);\n } else {\n this.soloedTracks.delete(trackId);\n }\n this.updateSoloMuting();\n }\n }\n\n private updateSoloMuting(): void {\n const hasSoloedTracks = this.soloedTracks.size > 0;\n\n this.tracks.forEach((track, id) => {\n if (hasSoloedTracks) {\n if (!this.soloedTracks.has(id)) {\n track.setMute(true);\n } else {\n const manuallyMuted = this.manualMuteState.get(id) ?? false;\n track.setMute(manuallyMuted);\n }\n } else {\n const manuallyMuted = this.manualMuteState.get(id) ?? false;\n track.setMute(manuallyMuted);\n }\n });\n }\n\n setMute(trackId: string, muted: boolean): void {\n const track = this.tracks.get(trackId);\n if (track) {\n this.manualMuteState.set(trackId, muted);\n track.setMute(muted);\n }\n }\n\n setLoop(enabled: boolean, loopStart: number, loopEnd: number): void {\n // Update cached state first — play() uses these values to configure\n // the Transport on next start, so they must reflect the caller's intent\n // regardless of whether Transport property setting succeeds.\n this._loopEnabled = enabled;\n this._loopStart = loopStart;\n this._loopEnd = loopEnd;\n\n const transport = getTransport();\n try {\n // Set boundaries BEFORE enabling loop. Tone.js's _processTick checks\n // `ticks >= _loopEnd` on every tick. If we set transport.loop = true\n // first, a tick could fire before loopEnd is updated, seeing the stale\n // _loopEnd value (0 from Transport default) and wrapping immediately.\n transport.loopStart = loopStart;\n transport.loopEnd = loopEnd;\n transport.loop = enabled;\n } catch (err) {\n console.warn('[waveform-playlist] Error configuring Transport loop:', err);\n return;\n }\n\n if (enabled && !this._loopHandler) {\n this._loopHandler = () => {\n // On loop boundary: stop old sources, re-schedule fades, start mid-clip sources.\n // Event ordering in Transport's tick processing (Tone.js 15.x _processTick):\n // loopEnd → ticks reset → loopStart → loop → forEachAtTime(ticks)\n // Our loop handler fires BEFORE schedule callbacks, so:\n // 1. stopAllSources + cancelFades — clean slate\n // 2. startMidClipSources — for clips spanning loopStart boundary\n // 3. prepareFades — fresh fade envelopes\n // Then Transport fires schedule callbacks for clips at/after loopStart.\n const currentTime = now();\n this.tracks.forEach((track) => {\n try {\n track.stopAllSources();\n track.cancelFades();\n track.setScheduleGuardOffset(this._loopStart);\n track.startMidClipSources(this._loopStart, currentTime);\n track.prepareFades(currentTime, this._loopStart);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error re-scheduling track \"${track.id}\" on loop:`,\n err\n );\n }\n });\n };\n transport.on('loop', this._loopHandler);\n } else if (!enabled && this._loopHandler) {\n transport.off('loop', this._loopHandler);\n this._loopHandler = null;\n }\n }\n\n getCurrentTime(): number {\n return getTransport().seconds;\n }\n\n seekTo(time: number): void {\n getTransport().seconds = time;\n }\n\n dispose(): void {\n // Stop Transport and all active sources before disposing.\n // Without this, the global Transport singleton keeps firing scheduled\n // callbacks and native AudioBufferSourceNodes continue playing through\n // the shared AudioContext (e.g., during Docusaurus client-side navigation).\n try {\n this.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Error stopping Transport during dispose:', err);\n }\n\n this.tracks.forEach((track) => {\n try {\n track.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing track \"${track.id}\":`, err);\n }\n });\n this.tracks.clear();\n\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn('[waveform-playlist] Error during master effects cleanup:', err);\n }\n }\n\n try {\n this.masterVolume.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing master volume:', err);\n }\n }\n\n get context(): BaseContext {\n return getContext();\n }\n\n get sampleRate(): number {\n return getContext().sampleRate;\n }\n\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n}\n","import {\n Volume,\n Gain,\n Panner,\n ToneAudioNode,\n getDestination,\n getTransport,\n getContext,\n} from 'tone';\nimport { Track, type Fade } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, getUnderlyingAudioParam } from './fades';\n\nexport type TrackEffectsFunction = (\n graphEnd: Gain,\n masterGainNode: ToneAudioNode,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface ClipInfo {\n buffer: AudioBuffer;\n startTime: number; // When this clip starts in the track timeline (seconds)\n duration: number; // How long this clip plays (seconds)\n offset: number; // Where to start playing within the buffer (seconds)\n fadeIn?: Fade;\n fadeOut?: Fade;\n gain: number; // Clip-level gain\n}\n\nexport interface ToneTrackOptions {\n buffer?: AudioBuffer; // Legacy: single buffer (deprecated, use clips instead)\n clips?: ClipInfo[]; // Modern: array of clips\n track: Track;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n}\n\n/** Per-clip scheduling info and audio nodes */\ninterface ScheduledClip {\n clipInfo: ClipInfo;\n fadeGainNode: GainNode; // Native GainNode for per-clip fade envelope\n scheduleId: number; // Transport.schedule() event ID\n}\n\nexport class ToneTrack {\n private scheduledClips: ScheduledClip[];\n private activeSources: Set<AudioBufferSourceNode> = new Set();\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n // Guard against ghost tick schedule callbacks. After stop/start cycles with\n // loops, stale Clock._lastUpdate causes ticks from the previous cycle to fire\n // Transport.schedule() callbacks at past positions (e.g., time 0 clips fire\n // when starting at offset 5s). Clips before this offset are handled by\n // startMidClipSources(); schedule callbacks should only create sources for\n // clips at/after this offset.\n private _scheduleGuardOffset = 0;\n\n constructor(options: ToneTrackOptions) {\n this.track = options.track;\n\n // Create shared track-level Tone.js nodes\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n // Tone.js Panner defaults to channelCount: 1 + channelCountMode: 'explicit',\n // which forces stereo→mono downmix (1/√2 attenuation) before panning.\n // Override to channelCount: 2 to preserve stereo recordings.\n this.panNode = new Panner({ pan: options.track.stereoPan, channelCount: 2 });\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n\n // Chain shared Tone.js nodes: Volume → Pan → MuteGain\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create clips array - support both legacy single buffer and modern clips array\n const clipInfos: ClipInfo[] =\n options.clips ||\n (options.buffer\n ? [\n {\n buffer: options.buffer,\n startTime: 0,\n duration: options.buffer.duration,\n offset: 0,\n fadeIn: options.track.fadeIn,\n fadeOut: options.track.fadeOut,\n gain: 1,\n },\n ]\n : []);\n\n const transport = getTransport();\n const rawContext = getContext().rawContext as AudioContext;\n\n // Get the native AudioNode input of the Volume for native→Tone connection.\n // Volume.input is a Tone.js Gain<\"decibels\"> whose .input is the native GainNode.\n // Cast through unknown since Gain<\"decibels\"> and Gain<\"gain\"> don't overlap.\n const volumeNativeInput = (this.volumeNode.input as unknown as Gain).input;\n\n // Schedule each clip via Transport.schedule() with native AudioBufferSourceNode\n this.scheduledClips = clipInfos.map((clipInfo) => {\n // Native GainNode for per-clip fade envelope — created once, reused across play cycles\n const fadeGainNode = rawContext.createGain();\n fadeGainNode.gain.value = clipInfo.gain;\n fadeGainNode.connect(volumeNativeInput);\n\n // Schedule a permanent Transport event at the clip's absolute timeline position.\n // This callback fires on every play and every loop iteration when Transport\n // passes this point.\n const absTransportTime = this.track.startTime + clipInfo.startTime;\n const scheduleId = transport.schedule((audioContextTime: number) => {\n // Guard: ghost ticks from stale Clock._lastUpdate can fire this callback\n // at past positions (see Tone.js #1419). Clips before the play/loop offset\n // are already handled by startMidClipSources().\n if (absTransportTime < this._scheduleGuardOffset) {\n return;\n }\n this.startClipSource(clipInfo, fadeGainNode, audioContextTime);\n }, absTransportTime);\n\n return { clipInfo, fadeGainNode, scheduleId };\n });\n }\n\n /**\n * Create and start an AudioBufferSourceNode for a clip.\n * Sources are one-shot: each play or loop iteration creates a fresh one.\n */\n private startClipSource(\n clipInfo: ClipInfo,\n fadeGainNode: GainNode,\n audioContextTime: number,\n bufferOffset?: number,\n playDuration?: number\n ): void {\n const rawContext = getContext().rawContext as AudioContext;\n const source = rawContext.createBufferSource();\n source.buffer = clipInfo.buffer;\n source.connect(fadeGainNode);\n\n const offset = bufferOffset ?? clipInfo.offset;\n const duration = playDuration ?? clipInfo.duration;\n\n try {\n source.start(audioContextTime, offset, duration);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start source on track \"${this.id}\" ` +\n `(time=${audioContextTime}, offset=${offset}, duration=${duration}):`,\n err\n );\n source.disconnect();\n return;\n }\n\n this.activeSources.add(source);\n source.onended = () => {\n this.activeSources.delete(source);\n };\n }\n\n /**\n * Set the schedule guard offset. Schedule callbacks for clips before this\n * offset are suppressed (already handled by startMidClipSources).\n * Must be called before transport.start() and in the loop handler.\n */\n setScheduleGuardOffset(offset: number): void {\n this._scheduleGuardOffset = offset;\n }\n\n /**\n * Start sources for clips that span the given Transport position.\n * Used for mid-playback seeking and loop boundary handling where\n * Transport.schedule() callbacks have already passed.\n *\n * Uses strict < for absClipStart to avoid double-creation with\n * schedule callbacks at exact Transport position (e.g., loopStart).\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo, fadeGainNode } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n // Only handle clips that started before the transport position\n // but haven't ended yet (i.e., the transport is \"inside\" the clip)\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n const elapsed = transportOffset - absClipStart;\n const adjustedOffset = clipInfo.offset + elapsed;\n const remainingDuration = clipInfo.duration - elapsed;\n this.startClipSource(\n clipInfo,\n fadeGainNode,\n audioContextTime,\n adjustedOffset,\n remainingDuration\n );\n }\n }\n }\n\n /**\n * Stop all active AudioBufferSourceNodes and clear the set.\n * Native AudioBufferSourceNodes ignore Transport state changes —\n * they must be explicitly stopped.\n */\n stopAllSources(): void {\n this.activeSources.forEach((source) => {\n try {\n source.stop();\n } catch (err) {\n console.warn(`[waveform-playlist] Error stopping source on track \"${this.id}\":`, err);\n }\n });\n this.activeSources.clear();\n }\n\n /**\n * Schedule fade envelopes for a clip at the given AudioContext time.\n * Uses native GainNode.gain (AudioParam) directly — no _param workaround needed.\n */\n private scheduleFades(\n scheduled: ScheduledClip,\n clipStartTime: number,\n clipOffset: number = 0\n ): void {\n const { clipInfo, fadeGainNode } = scheduled;\n const audioParam = fadeGainNode.gain;\n\n // Cancel any previous automation\n audioParam.cancelScheduledValues(0);\n\n // Calculate how much of the clip we're skipping (for seeking)\n const skipTime = clipOffset - clipInfo.offset;\n\n // Apply fade in if it exists and we haven't skipped past it\n if (clipInfo.fadeIn && skipTime < clipInfo.fadeIn.duration) {\n const fadeInDuration = clipInfo.fadeIn.duration;\n\n if (skipTime <= 0) {\n applyFadeIn(\n audioParam,\n clipStartTime,\n fadeInDuration,\n clipInfo.fadeIn.type || 'linear',\n 0,\n clipInfo.gain\n );\n } else {\n const remainingFadeDuration = fadeInDuration - skipTime;\n const fadeProgress = skipTime / fadeInDuration;\n const startValue = clipInfo.gain * fadeProgress;\n applyFadeIn(\n audioParam,\n clipStartTime,\n remainingFadeDuration,\n clipInfo.fadeIn.type || 'linear',\n startValue,\n clipInfo.gain\n );\n }\n } else {\n audioParam.setValueAtTime(clipInfo.gain, clipStartTime);\n }\n\n // Apply fade out if it exists\n if (clipInfo.fadeOut) {\n const fadeOutStart = clipInfo.duration - clipInfo.fadeOut.duration;\n const fadeOutStartInClip = fadeOutStart - skipTime;\n\n if (fadeOutStartInClip > 0) {\n const absoluteFadeOutStart = clipStartTime + fadeOutStartInClip;\n applyFadeOut(\n audioParam,\n absoluteFadeOutStart,\n clipInfo.fadeOut.duration,\n clipInfo.fadeOut.type || 'linear',\n clipInfo.gain,\n 0\n );\n } else if (fadeOutStartInClip > -clipInfo.fadeOut.duration) {\n const elapsedFadeOut = -fadeOutStartInClip;\n const remainingFadeDuration = clipInfo.fadeOut.duration - elapsedFadeOut;\n const fadeProgress = elapsedFadeOut / clipInfo.fadeOut.duration;\n const startValue = clipInfo.gain * (1 - fadeProgress);\n applyFadeOut(\n audioParam,\n clipStartTime,\n remainingFadeDuration,\n clipInfo.fadeOut.type || 'linear',\n startValue,\n 0\n );\n }\n }\n }\n\n /**\n * Prepare fade envelopes for all clips based on Transport offset.\n * Called before Transport.start() to schedule fades at correct AudioContext times.\n */\n prepareFades(when: number, transportOffset: number): void {\n this.scheduledClips.forEach((scheduled) => {\n const absClipStart = this.track.startTime + scheduled.clipInfo.startTime;\n const absClipEnd = absClipStart + scheduled.clipInfo.duration;\n\n if (transportOffset >= absClipEnd) return; // clip already finished\n\n if (transportOffset >= absClipStart) {\n // Mid-clip: playing now\n const clipOffset = transportOffset - absClipStart + scheduled.clipInfo.offset;\n this.scheduleFades(scheduled, when, clipOffset);\n } else {\n // Clip starts later\n const delay = absClipStart - transportOffset;\n this.scheduleFades(scheduled, when + delay, scheduled.clipInfo.offset);\n }\n });\n }\n\n /**\n * Cancel all scheduled fade automation and reset to nominal gain.\n * Called on pause/stop to prevent stale fade envelopes.\n */\n cancelFades(): void {\n this.scheduledClips.forEach(({ fadeGainNode, clipInfo }) => {\n const audioParam = fadeGainNode.gain;\n audioParam.cancelScheduledValues(0);\n audioParam.setValueAtTime(clipInfo.gain, 0);\n });\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n // Use setValueAtTime on the raw AudioParam to ensure the value is applied\n // even when the AudioContext is suspended. Setting .gain.value on the Tone.js\n // Signal wrapper doesn't propagate to the underlying AudioParam until the\n // context resumes, causing a brief audio glitch (e.g., all tracks audible\n // before solo muting takes effect).\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n const transport = getTransport();\n\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(`[waveform-playlist] Error during track \"${this.id}\" effects cleanup:`, err);\n }\n }\n\n this.stopAllSources();\n\n // Clear Transport schedule events and disconnect native fade gain nodes\n this.scheduledClips.forEach((scheduled, index) => {\n try {\n transport.clear(scheduled.scheduleId);\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error clearing schedule ${index} on track \"${this.id}\":`,\n err\n );\n }\n try {\n scheduled.fadeGainNode.disconnect();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disconnecting fadeGain ${index} on track \"${this.id}\":`,\n err\n );\n }\n });\n\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing volumeNode on track \"${this.id}\":`, err);\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing panNode on track \"${this.id}\":`, err);\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing muteGain on track \"${this.id}\":`, err);\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get buffer(): AudioBuffer {\n return this.scheduledClips[0]?.clipInfo.buffer;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","/**\n * Fade utilities — re-exports from @waveform-playlist/core\n * plus Tone.js-specific helpers\n */\n\n// Re-export all pure fade utilities from core\nexport {\n linearCurve,\n exponentialCurve,\n sCurveCurve,\n logarithmicCurve,\n generateCurve,\n applyFadeIn,\n applyFadeOut,\n} from '@waveform-playlist/core';\n\nexport type { FadeType, FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Access the underlying Web Audio AudioParam from a Tone.js Signal/Param wrapper.\n *\n * Tone.js wraps native AudioParam in its Signal class, but sometimes we need\n * direct access to the raw AudioParam for setValueAtTime/cancelScheduledValues\n * (e.g., when the AudioContext is suspended and Tone.js Signal doesn't propagate).\n *\n * This uses `_param` which is a private Tone.js 15.x internal.\n * Pin the Tone.js version carefully if upgrading.\n *\n * @param signal - A Tone.js Signal or Param wrapper (e.g., `gain.gain`)\n * @returns The underlying AudioParam, or undefined if not found\n */\nlet hasWarned = false;\n\nexport function getUnderlyingAudioParam(signal: unknown): AudioParam | undefined {\n const param = (signal as { _param?: AudioParam })._param;\n if (!param && !hasWarned) {\n hasWarned = true;\n console.warn(\n '[waveform-playlist] Unable to access Tone.js internal _param. ' +\n 'This likely means the Tone.js version is incompatible. ' +\n 'Mute scheduling may not work correctly.'\n );\n }\n return param;\n}\n","import {\n Volume,\n Gain,\n Panner,\n PolySynth,\n Synth,\n MembraneSynth,\n MetalSynth,\n NoiseSynth,\n Part,\n ToneAudioNode,\n getDestination,\n getContext,\n} from 'tone';\nimport type { SynthOptions } from 'tone';\nimport { Track } from '@waveform-playlist/core';\nimport type { MidiNoteData } from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam } from './fades';\nimport type { TrackEffectsFunction } from './ToneTrack';\n\n/**\n * Shared interface for tracks managed by TonePlayout.\n * Both ToneTrack (audio) and MidiToneTrack (MIDI) implement this,\n * allowing TonePlayout to manage them uniformly.\n */\nexport interface PlayableTrack {\n id: string;\n startTime: number;\n muted: boolean;\n duration: number;\n stopAllSources(): void;\n startMidClipSources(offset: number, time: number): void;\n setScheduleGuardOffset(offset: number): void;\n prepareFades(when: number, offset: number): void;\n cancelFades(): void;\n setVolume(gain: number): void;\n setPan(pan: number): void;\n setMute(muted: boolean): void;\n setSolo(soloed: boolean): void;\n dispose(): void;\n}\n\nexport interface MidiClipInfo {\n notes: MidiNoteData[];\n startTime: number; // When this clip starts relative to track start (seconds)\n duration: number; // Clip duration (seconds)\n offset: number; // Trim offset into the MIDI data (seconds)\n}\n\nexport interface MidiToneTrackOptions {\n clips: MidiClipInfo[];\n track: Track;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n synthOptions?: Partial<SynthOptions>;\n}\n\n/**\n * Categorize GM percussion note numbers into synth types.\n * See: https://www.midi.org/specifications-old/item/gm-level-1-sound-set\n */\ntype DrumCategory = 'kick' | 'snare' | 'cymbal' | 'tom';\n\nfunction getDrumCategory(midiNote: number): DrumCategory {\n // Bass drums\n if (midiNote === 35 || midiNote === 36) return 'kick';\n // Snare, side stick, clap\n if (midiNote >= 37 && midiNote <= 40) return 'snare';\n // Toms (low floor tom through high tom)\n if (\n midiNote === 41 ||\n midiNote === 43 ||\n midiNote === 45 ||\n midiNote === 47 ||\n midiNote === 48 ||\n midiNote === 50\n )\n return 'tom';\n // Hi-hats, cymbals, bells, tambourine, cowbell, etc.\n return 'cymbal';\n}\n\n/** Per-clip scheduling info */\ninterface ScheduledMidiClip {\n clipInfo: MidiClipInfo;\n part: Part;\n}\n\n/**\n * MIDI track that always creates both melodic and percussion synths.\n * Per-note routing uses the `channel` field on each MidiNoteData:\n * channel 9 → percussion synths, all others → melodic PolySynth.\n * This enables flattened tracks (mixed channels) to play correctly.\n */\nexport class MidiToneTrack implements PlayableTrack {\n private scheduledClips: ScheduledMidiClip[];\n // Melodic synth — always created\n private synth: PolySynth;\n // Percussion synths — always created (PolySynth wrappers for polyphony)\n private kickSynth: PolySynth<MembraneSynth>;\n private snareSynth: NoiseSynth; // No pitch param, can't wrap in PolySynth\n private cymbalSynth: PolySynth<MetalSynth>;\n private tomSynth: PolySynth<MembraneSynth>;\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n\n constructor(options: MidiToneTrackOptions) {\n this.track = options.track;\n\n // Create shared track-level Tone.js nodes (same chain as ToneTrack)\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n this.panNode = new Panner(options.track.stereoPan);\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Melodic: PolySynth with basic Synth voice\n this.synth = new PolySynth(Synth, options.synthOptions);\n this.synth.connect(this.volumeNode);\n\n // Percussion: PolySynth wrappers for polyphonic playback\n this.kickSynth = new PolySynth(MembraneSynth, {\n voice: MembraneSynth,\n options: {\n pitchDecay: 0.05,\n octaves: 6,\n envelope: { attack: 0.001, decay: 0.4, sustain: 0, release: 0.1 },\n },\n } as never);\n this.snareSynth = new NoiseSynth({\n noise: { type: 'white' },\n envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.05 },\n });\n this.cymbalSynth = new PolySynth(MetalSynth, {\n voice: MetalSynth,\n options: {\n envelope: { attack: 0.001, decay: 0.3, release: 0.1 },\n harmonicity: 5.1,\n modulationIndex: 32,\n resonance: 4000,\n octaves: 1.5,\n },\n } as never);\n this.tomSynth = new PolySynth(MembraneSynth, {\n voice: MembraneSynth,\n options: {\n pitchDecay: 0.08,\n octaves: 4,\n envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.1 },\n },\n } as never);\n\n this.kickSynth.connect(this.volumeNode);\n this.snareSynth.connect(this.volumeNode);\n this.cymbalSynth.connect(this.volumeNode);\n this.tomSynth.connect(this.volumeNode);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create a Tone.Part for each clip, scheduling notes relative to Transport\n this.scheduledClips = options.clips.map((clipInfo) => {\n // Filter notes within the clip's visible window (after trim offset)\n const visibleNotes = clipInfo.notes.filter((note) => {\n const noteEnd = note.time + note.duration;\n // Note must start before clip ends and end after clip's trim offset\n return note.time < clipInfo.offset + clipInfo.duration && noteEnd > clipInfo.offset;\n });\n\n // Create Part events with absolute Transport times\n const absClipStart = this.track.startTime + clipInfo.startTime;\n\n const partEvents = visibleNotes.map((note) => {\n // Adjust note timing relative to clip's trim offset\n const adjustedTime = note.time - clipInfo.offset;\n // Clamp to clip boundaries\n const clampedStart = Math.max(0, adjustedTime);\n const clampedDuration = Math.min(\n note.duration - Math.max(0, clipInfo.offset - note.time),\n clipInfo.duration - clampedStart\n );\n\n return {\n time: absClipStart + clampedStart,\n note: note.name,\n midi: note.midi,\n duration: Math.max(0, clampedDuration),\n velocity: note.velocity,\n channel: note.channel,\n };\n });\n\n const part = new Part((time, event) => {\n if (event.duration > 0) {\n this.triggerNote(\n event.midi,\n event.note,\n event.duration,\n time,\n event.velocity,\n event.channel\n );\n }\n }, partEvents);\n\n // Part starts automatically with Transport\n part.start(0);\n\n return { clipInfo, part };\n });\n }\n\n /**\n * Trigger a note using the appropriate synth.\n * Routes per-note: channel 9 → percussion synths, others → melodic PolySynth.\n */\n private triggerNote(\n midiNote: number,\n noteName: string,\n duration: number,\n time: number,\n velocity: number,\n channel?: number\n ): void {\n if (channel === 9) {\n const category = getDrumCategory(midiNote);\n switch (category) {\n case 'kick':\n this.kickSynth.triggerAttackRelease('C1', duration, time, velocity);\n break;\n case 'snare':\n // NoiseSynth is monophonic — wrap in try-catch for rare overlaps\n try {\n this.snareSynth.triggerAttackRelease(duration, time, velocity);\n } catch (err) {\n console.warn(\n '[waveform-playlist] Snare overlap — previous hit still decaying, skipped:',\n err\n );\n }\n break;\n case 'tom': {\n const tomPitches: Record<number, string> = {\n 41: 'G1',\n 43: 'A1',\n 45: 'C2',\n 47: 'D2',\n 48: 'E2',\n 50: 'G2',\n };\n this.tomSynth.triggerAttackRelease(\n tomPitches[midiNote] || 'C2',\n duration,\n time,\n velocity\n );\n break;\n }\n case 'cymbal':\n // PolySynth requires a note arg; MetalSynth uses it as fundamental frequency.\n // 'C4' provides a reasonable metallic timbre for all cymbal/hi-hat hits.\n this.cymbalSynth.triggerAttackRelease('C4', duration, time, velocity);\n break;\n }\n } else {\n this.synth.triggerAttackRelease(noteName, duration, time, velocity);\n }\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n /**\n * No-op for MIDI — schedule guard is for AudioBufferSourceNode ghost tick prevention.\n * Tone.Part handles its own scheduling relative to Transport.\n */\n setScheduleGuardOffset(_offset: number): void {\n // No-op: Tone.Part handles scheduling internally\n }\n\n /**\n * For MIDI, mid-clip sources are notes that should already be sounding.\n * We trigger them with their remaining duration.\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n // Find notes that should be currently sounding\n for (const note of clipInfo.notes) {\n const adjustedTime = note.time - clipInfo.offset;\n const noteAbsStart = absClipStart + Math.max(0, adjustedTime);\n const noteAbsEnd = noteAbsStart + note.duration;\n\n if (noteAbsStart < transportOffset && noteAbsEnd > transportOffset) {\n const remainingDuration = noteAbsEnd - transportOffset;\n try {\n this.triggerNote(\n note.midi,\n note.name,\n remainingDuration,\n audioContextTime,\n note.velocity,\n note.channel\n );\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start mid-clip MIDI note \"${note.name}\" on track \"${this.id}\":`,\n err\n );\n }\n }\n }\n }\n }\n }\n\n /**\n * Stop all sounding notes and cancel scheduled Part events.\n */\n stopAllSources(): void {\n const now = getContext().rawContext.currentTime;\n try {\n this.synth.releaseAll(now);\n this.kickSynth.releaseAll(now);\n this.cymbalSynth.releaseAll(now);\n this.tomSynth.releaseAll(now);\n // NoiseSynth has no releaseAll — it decays naturally via short envelope\n } catch (err) {\n console.warn(`[waveform-playlist] Error releasing synth on track \"${this.id}\":`, err);\n }\n }\n\n /**\n * No-op for MIDI — MIDI uses note velocity, not gain fades.\n */\n prepareFades(_when: number, _offset: number): void {\n // No-op: MIDI clips don't use fade envelopes\n }\n\n /**\n * No-op for MIDI — no fade automation to cancel.\n */\n cancelFades(): void {\n // No-op: MIDI clips don't use fade envelopes\n }\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error during MIDI track \"${this.id}\" effects cleanup:`,\n err\n );\n }\n }\n\n this.stopAllSources();\n\n // Dispose Parts\n this.scheduledClips.forEach(({ part }, index) => {\n try {\n part.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing Part ${index} on MIDI track \"${this.id}\":`,\n err\n );\n }\n });\n\n // Dispose all synths\n const synthsToDispose = [\n this.synth,\n this.kickSynth,\n this.snareSynth,\n this.cymbalSynth,\n this.tomSynth,\n ];\n for (const s of synthsToDispose) {\n try {\n s?.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing synth on MIDI track \"${this.id}\":`, err);\n }\n }\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing volumeNode on MIDI track \"${this.id}\":`,\n err\n );\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing panNode on MIDI track \"${this.id}\":`, err);\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(`[waveform-playlist] Error disposing muteGain on MIDI track \"${this.id}\":`, err);\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","import { Volume, Gain, Panner, Part, ToneAudioNode, getDestination, getContext } from 'tone';\nimport { Track } from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam } from './fades';\nimport type { TrackEffectsFunction } from './ToneTrack';\nimport type { PlayableTrack, MidiClipInfo } from './MidiToneTrack';\nimport type { SoundFontCache } from './SoundFontCache';\n\nexport interface SoundFontToneTrackOptions {\n clips: MidiClipInfo[];\n track: Track;\n soundFontCache: SoundFontCache;\n /** GM program number (0-127) for melodic instruments */\n programNumber?: number;\n /** Whether this track uses percussion bank (channel 9) */\n isPercussion?: boolean;\n effects?: TrackEffectsFunction;\n destination?: ToneAudioNode;\n}\n\n/** Per-clip scheduling info */\ninterface ScheduledMidiClip {\n clipInfo: MidiClipInfo;\n part: Part;\n}\n\n/**\n * MIDI track that uses SoundFont samples for playback.\n *\n * Instead of PolySynth synthesis, each note triggers the correct instrument\n * sample from an SF2 file, pitch-shifted via AudioBufferSourceNode.playbackRate.\n *\n * Audio graph per note:\n * AudioBufferSourceNode (native, one-shot, pitch-shifted)\n * → GainNode (native, per-note velocity)\n * → Volume.input (Tone.js, shared per-track)\n * → Panner → muteGain → effects/destination\n */\nexport class SoundFontToneTrack implements PlayableTrack {\n /** Rate-limit missing sample warnings — one per class lifetime */\n private static _missingSampleWarned = false;\n private scheduledClips: ScheduledMidiClip[];\n private activeSources: Set<AudioBufferSourceNode> = new Set();\n private soundFontCache: SoundFontCache;\n private programNumber: number;\n private bankNumber: number;\n private volumeNode: Volume;\n private panNode: Panner;\n private muteGain: Gain;\n private track: Track;\n private effectsCleanup?: () => void;\n\n constructor(options: SoundFontToneTrackOptions) {\n this.track = options.track;\n this.soundFontCache = options.soundFontCache;\n this.programNumber = options.programNumber ?? 0;\n // Bank 128 for percussion (channel 9), bank 0 for melodic\n this.bankNumber = options.isPercussion ? 128 : 0;\n\n // Create shared track-level Tone.js nodes (same chain as ToneTrack)\n this.volumeNode = new Volume(this.gainToDb(options.track.gain));\n this.panNode = new Panner(options.track.stereoPan);\n this.muteGain = new Gain(options.track.muted ? 0 : 1);\n this.volumeNode.chain(this.panNode, this.muteGain);\n\n // Connect to destination or apply effects chain\n const destination = options.destination || getDestination();\n if (options.effects) {\n const cleanup = options.effects(this.muteGain, destination, false);\n if (cleanup) {\n this.effectsCleanup = cleanup;\n }\n } else {\n this.muteGain.connect(destination);\n }\n\n // Create a Tone.Part for each clip, scheduling notes relative to Transport\n this.scheduledClips = options.clips.map((clipInfo) => {\n // Filter notes within the clip's visible window (after trim offset)\n const visibleNotes = clipInfo.notes.filter((note) => {\n const noteEnd = note.time + note.duration;\n return note.time < clipInfo.offset + clipInfo.duration && noteEnd > clipInfo.offset;\n });\n\n const absClipStart = this.track.startTime + clipInfo.startTime;\n\n const partEvents = visibleNotes.map((note) => {\n const adjustedTime = note.time - clipInfo.offset;\n const clampedStart = Math.max(0, adjustedTime);\n const clampedDuration = Math.min(\n note.duration - Math.max(0, clipInfo.offset - note.time),\n clipInfo.duration - clampedStart\n );\n\n return {\n time: absClipStart + clampedStart,\n note: note.name,\n midi: note.midi,\n duration: Math.max(0, clampedDuration),\n velocity: note.velocity,\n channel: note.channel,\n };\n });\n\n const part = new Part((time, event) => {\n if (event.duration > 0) {\n this.triggerNote(event.midi, event.duration, time, event.velocity, event.channel);\n }\n }, partEvents);\n\n part.start(0);\n\n return { clipInfo, part };\n });\n }\n\n /**\n * Trigger a note by creating a native AudioBufferSourceNode from the SoundFont cache.\n *\n * Per-note routing: channel 9 → bank 128 (drums), others → bank 0 with programNumber.\n */\n private triggerNote(\n midiNote: number,\n duration: number,\n time: number,\n velocity: number,\n channel?: number\n ): void {\n const bank = channel === 9 ? 128 : this.bankNumber;\n const preset = channel === 9 ? 0 : this.programNumber;\n\n const sfSample = this.soundFontCache.getAudioBuffer(midiNote, bank, preset);\n if (!sfSample) {\n if (!SoundFontToneTrack._missingSampleWarned) {\n console.warn(\n `[waveform-playlist] SoundFont sample not found for MIDI note ${midiNote} (bank ${bank}, preset ${preset}). ` +\n 'Subsequent missing samples will be silent.'\n );\n SoundFontToneTrack._missingSampleWarned = true;\n }\n return;\n }\n\n const rawContext = getContext().rawContext as BaseAudioContext;\n\n // Create AudioBufferSourceNode with optional looping\n const source = rawContext.createBufferSource();\n source.buffer = sfSample.buffer;\n source.playbackRate.value = sfSample.playbackRate;\n\n // Enable sample looping if SF2 zone defines loop mode\n if (sfSample.loopMode === 1 || sfSample.loopMode === 3) {\n source.loop = true;\n source.loopStart = sfSample.loopStart;\n source.loopEnd = sfSample.loopEnd;\n }\n\n // For non-looping samples (percussion, one-shots), use the sample's natural\n // buffer duration so cymbals/drums ring out fully. For looping samples\n // (sustained instruments), use the MIDI note duration for note-off timing.\n const sampleDuration = sfSample.buffer.duration / sfSample.playbackRate;\n const effectiveDuration =\n sfSample.loopMode === 0 ? Math.max(duration, sampleDuration) : duration;\n\n // Per-note gain with SF2 volume ADSR envelope\n const peakGain = velocity * velocity;\n const gainNode = rawContext.createGain();\n const { attackVolEnv, holdVolEnv, decayVolEnv, sustainVolEnv, releaseVolEnv } = sfSample;\n const sustainGain = peakGain * sustainVolEnv;\n\n // Attack: start silent, ramp to peak\n gainNode.gain.setValueAtTime(0, time);\n gainNode.gain.linearRampToValueAtTime(peakGain, time + attackVolEnv);\n // Hold at peak\n if (holdVolEnv > 0.001) {\n gainNode.gain.setValueAtTime(peakGain, time + attackVolEnv + holdVolEnv);\n }\n // Decay to sustain level\n const decayStart = time + attackVolEnv + holdVolEnv;\n gainNode.gain.linearRampToValueAtTime(sustainGain, decayStart + decayVolEnv);\n // Sustain holds until note-off, then release ramps to silence.\n // For short notes (duration < AHD), setValueAtTime at note-off cancels the\n // incomplete decay ramp — Web Audio handles this correctly.\n gainNode.gain.setValueAtTime(sustainGain, time + effectiveDuration);\n gainNode.gain.linearRampToValueAtTime(0, time + effectiveDuration + releaseVolEnv);\n\n // Connect: source → gainNode → Volume.input (Tone.js)\n source.connect(gainNode);\n gainNode.connect((this.volumeNode.input as unknown as Gain).input);\n\n // Track active sources for stopAllSources()\n this.activeSources.add(source);\n source.onended = () => {\n this.activeSources.delete(source);\n try {\n gainNode.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] GainNode already disconnected:', err);\n }\n };\n\n source.start(time);\n source.stop(time + effectiveDuration + releaseVolEnv);\n }\n\n private gainToDb(gain: number): number {\n return 20 * Math.log10(gain);\n }\n\n /**\n * No-op — Tone.Part handles scheduling internally, no ghost tick guard needed.\n */\n setScheduleGuardOffset(_offset: number): void {\n // No-op\n }\n\n /**\n * Start notes that should already be sounding at the current transport offset.\n */\n startMidClipSources(transportOffset: number, audioContextTime: number): void {\n for (const { clipInfo } of this.scheduledClips) {\n const absClipStart = this.track.startTime + clipInfo.startTime;\n const absClipEnd = absClipStart + clipInfo.duration;\n\n if (absClipStart < transportOffset && absClipEnd > transportOffset) {\n for (const note of clipInfo.notes) {\n const adjustedTime = note.time - clipInfo.offset;\n const noteAbsStart = absClipStart + Math.max(0, adjustedTime);\n const noteAbsEnd = noteAbsStart + note.duration;\n\n if (noteAbsStart < transportOffset && noteAbsEnd > transportOffset) {\n const remainingDuration = noteAbsEnd - transportOffset;\n try {\n this.triggerNote(\n note.midi,\n remainingDuration,\n audioContextTime,\n note.velocity,\n note.channel\n );\n } catch (err) {\n console.warn(\n `[waveform-playlist] Failed to start mid-clip SoundFont note on track \"${this.id}\":`,\n err\n );\n }\n }\n }\n }\n }\n }\n\n /**\n * Stop all active AudioBufferSourceNodes.\n */\n stopAllSources(): void {\n for (const source of this.activeSources) {\n try {\n source.stop();\n } catch (err) {\n console.warn('[waveform-playlist] Error stopping AudioBufferSourceNode:', err);\n }\n }\n this.activeSources.clear();\n }\n\n /** No-op for MIDI — MIDI uses note velocity, not gain fades. */\n prepareFades(_when: number, _offset: number): void {}\n\n /** No-op for MIDI — no fade automation to cancel. */\n cancelFades(): void {}\n\n setVolume(gain: number): void {\n this.track.gain = gain;\n this.volumeNode.volume.value = this.gainToDb(gain);\n }\n\n setPan(pan: number): void {\n this.track.stereoPan = pan;\n this.panNode.pan.value = pan;\n }\n\n setMute(muted: boolean): void {\n this.track.muted = muted;\n const value = muted ? 0 : 1;\n const audioParam = getUnderlyingAudioParam(this.muteGain.gain);\n audioParam?.setValueAtTime(value, 0);\n this.muteGain.gain.value = value;\n }\n\n setSolo(soloed: boolean): void {\n this.track.soloed = soloed;\n }\n\n dispose(): void {\n if (this.effectsCleanup) {\n try {\n this.effectsCleanup();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error during SoundFont track \"${this.id}\" effects cleanup:`,\n err\n );\n }\n }\n\n this.stopAllSources();\n\n // Dispose Parts\n this.scheduledClips.forEach(({ part }, index) => {\n try {\n part.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing Part ${index} on SoundFont track \"${this.id}\":`,\n err\n );\n }\n });\n\n try {\n this.volumeNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing volumeNode on SoundFont track \"${this.id}\":`,\n err\n );\n }\n try {\n this.panNode.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing panNode on SoundFont track \"${this.id}\":`,\n err\n );\n }\n try {\n this.muteGain.dispose();\n } catch (err) {\n console.warn(\n `[waveform-playlist] Error disposing muteGain on SoundFont track \"${this.id}\":`,\n err\n );\n }\n }\n\n get id(): string {\n return this.track.id;\n }\n\n get duration(): number {\n if (this.scheduledClips.length === 0) return 0;\n const lastClip = this.scheduledClips[this.scheduledClips.length - 1];\n return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;\n }\n\n get muted(): boolean {\n return this.track.muted;\n }\n\n get startTime(): number {\n return this.track.startTime;\n }\n}\n","import { SoundFont2, GeneratorType } from 'soundfont2';\nimport type { Generator, ZoneMap } from 'soundfont2';\n\n/**\n * Result of looking up a MIDI note in the SoundFont.\n * Contains the AudioBuffer, playbackRate, loop points, and volume envelope.\n */\nexport interface SoundFontSample {\n /** Cached AudioBuffer for this sample */\n buffer: AudioBuffer;\n /** Playback rate to pitch-shift from originalPitch to target note */\n playbackRate: number;\n /** Loop mode: 0=no loop, 1=continuous, 3=sustain loop */\n loopMode: number;\n /** Loop start in seconds, relative to AudioBuffer start */\n loopStart: number;\n /** Loop end in seconds, relative to AudioBuffer start */\n loopEnd: number;\n /** Volume envelope attack time in seconds */\n attackVolEnv: number;\n /** Volume envelope hold time in seconds */\n holdVolEnv: number;\n /** Volume envelope decay time in seconds */\n decayVolEnv: number;\n /** Volume envelope sustain level as linear gain 0-1 */\n sustainVolEnv: number;\n /** Volume envelope release time in seconds */\n releaseVolEnv: number;\n}\n\n/**\n * Convert SF2 timecents to seconds.\n * SF2 formula: seconds = 2^(timecents / 1200)\n * Default -12000 timecents ≈ 0.001s (effectively instant).\n */\nexport function timecentsToSeconds(tc: number): number {\n return Math.pow(2, tc / 1200);\n}\n\n/** Max release time to prevent extremely long tails from stale generators */\nconst MAX_RELEASE_SECONDS = 5;\n\n/**\n * Get a numeric generator value from a zone map.\n */\nexport function getGeneratorValue(\n generators: ZoneMap<Generator>,\n type: GeneratorType\n): number | undefined {\n return generators[type]?.value;\n}\n\n/**\n * Convert Int16Array sample data to Float32Array.\n * SF2 samples are 16-bit signed integers; Web Audio needs Float32 [-1, 1].\n */\nexport function int16ToFloat32(samples: Int16Array): Float32Array {\n const floats = new Float32Array(samples.length);\n for (let i = 0; i < samples.length; i++) {\n floats[i] = samples[i] / 32768;\n }\n return floats;\n}\n\n/**\n * Input parameters for playback rate calculation.\n */\nexport interface PlaybackRateParams {\n /** Target MIDI note number (0-127) */\n midiNote: number;\n /** OverridingRootKey generator value, or undefined if not set */\n overrideRootKey: number | undefined;\n /** sample.header.originalPitch (255 means unpitched) */\n originalPitch: number;\n /** CoarseTune generator value in semitones (default 0) */\n coarseTune: number;\n /** FineTune generator value in cents (default 0) */\n fineTune: number;\n /** sample.header.pitchCorrection in cents (default 0) */\n pitchCorrection: number;\n}\n\n/**\n * Calculate playback rate for a MIDI note using the SF2 generator chain.\n *\n * SF2 root key resolution priority:\n * 1. OverridingRootKey generator (per-zone, most specific)\n * 2. sample.header.originalPitch (sample header)\n * 3. MIDI note 60 (middle C fallback)\n *\n * Tuning adjustments:\n * - CoarseTune generator (semitones, additive)\n * - FineTune generator (cents, additive)\n * - sample.header.pitchCorrection (cents, additive)\n */\nexport function calculatePlaybackRate(params: PlaybackRateParams): number {\n const { midiNote, overrideRootKey, originalPitch, coarseTune, fineTune, pitchCorrection } =\n params;\n\n // Resolve root key: OverridingRootKey → originalPitch → 60\n const rootKey =\n overrideRootKey !== undefined ? overrideRootKey : originalPitch !== 255 ? originalPitch : 60;\n\n // Total offset in semitones: target note - root key + tuning\n const totalSemitones = midiNote - rootKey + coarseTune + (fineTune + pitchCorrection) / 100;\n\n return Math.pow(2, totalSemitones / 12);\n}\n\n/**\n * Input parameters for loop and envelope extraction.\n */\nexport interface LoopAndEnvelopeParams {\n /** SF2 generators zone map */\n generators: ZoneMap<Generator>;\n /** Sample header with loop points and sample rate */\n header: {\n startLoop: number;\n endLoop: number;\n sampleRate: number;\n };\n}\n\n/**\n * Extract loop points and volume envelope data from per-zone generators.\n *\n * Loop points are stored as absolute indices into the SF2 sample pool.\n * We convert to AudioBuffer-relative seconds by subtracting header.start\n * and dividing by sampleRate.\n *\n * Volume envelope times are in SF2 timecents; sustain is centibels attenuation.\n */\nexport function extractLoopAndEnvelope(\n params: LoopAndEnvelopeParams\n): Omit<SoundFontSample, 'buffer' | 'playbackRate'> {\n const { generators, header } = params;\n\n // --- Loop points ---\n const loopMode = getGeneratorValue(generators, GeneratorType.SampleModes) ?? 0;\n\n // Compute actual loop positions (header + fine/coarse generator offsets)\n const rawLoopStart =\n header.startLoop +\n (getGeneratorValue(generators, GeneratorType.StartLoopAddrsOffset) ?? 0) +\n (getGeneratorValue(generators, GeneratorType.StartLoopAddrsCoarseOffset) ?? 0) * 32768;\n const rawLoopEnd =\n header.endLoop +\n (getGeneratorValue(generators, GeneratorType.EndLoopAddrsOffset) ?? 0) +\n (getGeneratorValue(generators, GeneratorType.EndLoopAddrsCoarseOffset) ?? 0) * 32768;\n\n // The soundfont2 library already converts startLoop/endLoop to be\n // relative to sample.data (subtracts header.start during parsing),\n // so we only need to divide by sampleRate to get seconds.\n const loopStart = rawLoopStart / header.sampleRate;\n const loopEnd = rawLoopEnd / header.sampleRate;\n\n // --- Volume envelope ---\n const attackVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.AttackVolEnv) ?? -12000\n );\n const holdVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.HoldVolEnv) ?? -12000\n );\n const decayVolEnv = timecentsToSeconds(\n getGeneratorValue(generators, GeneratorType.DecayVolEnv) ?? -12000\n );\n const releaseVolEnv = Math.min(\n timecentsToSeconds(getGeneratorValue(generators, GeneratorType.ReleaseVolEnv) ?? -12000),\n MAX_RELEASE_SECONDS\n );\n\n // SustainVolEnv is centibels attenuation: 0 = full volume, 1440 = silence\n // Convert to linear gain: 10^(-cb / 200)\n const sustainCb = getGeneratorValue(generators, GeneratorType.SustainVolEnv) ?? 0;\n const sustainVolEnv = Math.pow(10, -sustainCb / 200);\n\n return {\n loopMode,\n loopStart,\n loopEnd,\n attackVolEnv,\n holdVolEnv,\n decayVolEnv,\n sustainVolEnv,\n releaseVolEnv,\n };\n}\n\n/**\n * Caches parsed SoundFont2 data and AudioBuffers for efficient playback.\n *\n * AudioBuffers are created lazily on first access and cached by sample index.\n * Pitch calculation uses the SF2 generator chain:\n * OverridingRootKey → sample.header.originalPitch → fallback 60\n *\n * Audio graph per note:\n * AudioBufferSourceNode (playbackRate for pitch) → GainNode (velocity) → track chain\n */\nexport class SoundFontCache {\n private sf2: SoundFont2 | null = null;\n private audioBufferCache: Map<number, AudioBuffer> = new Map();\n private context: BaseAudioContext;\n\n /**\n * @param context Optional AudioContext for createBuffer(). If omitted, uses\n * an OfflineAudioContext which doesn't require user gesture — safe to\n * construct before user interaction (avoids Firefox autoplay warnings).\n */\n constructor(context?: BaseAudioContext) {\n // OfflineAudioContext only needs valid params; we never call startRendering().\n // It's used solely for createBuffer() which works identically to AudioContext.\n this.context = context ?? new OfflineAudioContext(1, 1, 44100);\n }\n\n /**\n * Load and parse an SF2 file from a URL.\n */\n async load(url: string, signal?: AbortSignal): Promise<void> {\n const response = await fetch(url, { signal });\n if (!response.ok) {\n throw new Error(`Failed to fetch SoundFont ${url}: ${response.statusText}`);\n }\n const arrayBuffer = await response.arrayBuffer();\n try {\n this.sf2 = new SoundFont2(new Uint8Array(arrayBuffer));\n } catch (err) {\n throw new Error(\n `Failed to parse SoundFont ${url}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n /**\n * Load from an already-fetched ArrayBuffer.\n */\n loadFromBuffer(data: ArrayBuffer): void {\n try {\n this.sf2 = new SoundFont2(new Uint8Array(data));\n } catch (err) {\n throw new Error(\n `Failed to parse SoundFont from buffer: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n get isLoaded(): boolean {\n return this.sf2 !== null;\n }\n\n /**\n * Look up a MIDI note and return the AudioBuffer + playbackRate.\n *\n * @param midiNote - MIDI note number (0-127)\n * @param bankNumber - Bank number (0 for melodic, 128 for percussion/drums)\n * @param presetNumber - GM program number (0-127)\n * @returns SoundFontSample or null if no sample found for this note\n */\n getAudioBuffer(\n midiNote: number,\n bankNumber: number = 0,\n presetNumber: number = 0\n ): SoundFontSample | null {\n if (!this.sf2) return null;\n\n const keyData = this.sf2.getKeyData(midiNote, bankNumber, presetNumber);\n if (!keyData) return null;\n\n const sample = keyData.sample;\n const sampleIndex = this.sf2.samples.indexOf(sample);\n\n // Get or create the AudioBuffer for this sample\n let buffer = this.audioBufferCache.get(sampleIndex);\n if (!buffer) {\n buffer = this.int16ToAudioBuffer(sample.data, sample.header.sampleRate);\n this.audioBufferCache.set(sampleIndex, buffer);\n }\n\n // Calculate playback rate using SF2 generator chain for root key.\n // Priority: OverridingRootKey generator → sample.header.originalPitch → 60\n const playbackRate = calculatePlaybackRate({\n midiNote,\n overrideRootKey: getGeneratorValue(keyData.generators, GeneratorType.OverridingRootKey),\n originalPitch: sample.header.originalPitch,\n coarseTune: getGeneratorValue(keyData.generators, GeneratorType.CoarseTune) ?? 0,\n fineTune: getGeneratorValue(keyData.generators, GeneratorType.FineTune) ?? 0,\n pitchCorrection: sample.header.pitchCorrection ?? 0,\n });\n\n // Extract per-zone loop points and volume envelope from generators\n const loopAndEnvelope = extractLoopAndEnvelope({\n generators: keyData.generators,\n header: keyData.sample.header,\n });\n\n return { buffer, playbackRate, ...loopAndEnvelope };\n }\n\n /**\n * Convert Int16Array sample data to an AudioBuffer.\n * Uses the extracted int16ToFloat32 for the conversion, then copies into an AudioBuffer.\n */\n private int16ToAudioBuffer(data: Int16Array, sampleRate: number): AudioBuffer {\n const floats = int16ToFloat32(data);\n const buffer = this.context.createBuffer(1, floats.length, sampleRate);\n buffer.getChannelData(0).set(floats);\n return buffer;\n }\n\n /**\n * Clear all cached AudioBuffers and release the parsed SF2.\n */\n dispose(): void {\n this.audioBufferCache.clear();\n this.sf2 = null;\n }\n}\n","/**\n * Global AudioContext Manager\n *\n * Provides a single AudioContext shared across the entire application.\n * This context is used by Tone.js for playback and by all recording/monitoring hooks.\n *\n * Uses Tone.js's Context class which wraps standardized-audio-context for\n * cross-browser compatibility (fixes Firefox AudioListener issues).\n */\n\nimport { Context, setContext } from 'tone';\n\nlet globalToneContext: Context | null = null;\n\n/**\n * Get the global Tone.js Context\n * This is the main context for cross-browser audio operations.\n * Use context.createAudioWorkletNode(), context.createMediaStreamSource(), etc.\n * @returns The Tone.js Context instance\n */\nexport function getGlobalContext(): Context {\n if (!globalToneContext) {\n globalToneContext = new Context();\n setContext(globalToneContext);\n }\n return globalToneContext;\n}\n\n/**\n * Get or create the global AudioContext\n * Uses Tone.js Context for cross-browser compatibility\n * @returns The global AudioContext instance (rawContext from Tone.Context)\n */\nexport function getGlobalAudioContext(): AudioContext {\n return getGlobalContext().rawContext as AudioContext;\n}\n\n/**\n * @deprecated Use getGlobalContext() instead\n * Get the Tone.js Context's rawContext typed as IAudioContext\n * @returns The rawContext cast as IAudioContext\n */\nexport function getGlobalToneContext(): Context {\n return getGlobalContext();\n}\n\n/**\n * Resume the global AudioContext if it's suspended\n * Should be called in response to a user gesture (e.g., button click)\n * @returns Promise that resolves when context is running\n */\nexport async function resumeGlobalAudioContext(): Promise<void> {\n const context = getGlobalContext();\n if (context.state !== 'running') {\n await context.resume();\n }\n}\n\n/**\n * Get the current state of the global AudioContext\n * @returns The AudioContext state ('suspended', 'running', or 'closed')\n */\nexport function getGlobalAudioContextState(): AudioContextState {\n return globalToneContext?.rawContext.state || 'suspended';\n}\n\n/**\n * Close the global AudioContext\n * Should only be called when the application is shutting down\n */\nexport async function closeGlobalAudioContext(): Promise<void> {\n if (globalToneContext && globalToneContext.rawContext.state !== 'closed') {\n await globalToneContext.close();\n globalToneContext = null;\n }\n}\n","/**\n * MediaStreamSource Manager\n *\n * Manages MediaStreamAudioSourceNode instances to ensure only one source\n * is created per MediaStream per AudioContext.\n *\n * Web Audio API constraint: You can only create one MediaStreamAudioSourceNode\n * per MediaStream per AudioContext. Multiple attempts will fail or disconnect\n * previous sources.\n *\n * This manager ensures a single source is shared across multiple consumers\n * (e.g., AnalyserNode for VU meter, AudioWorkletNode for recording).\n *\n * NOTE: With Tone.js Context, you can also use context.createMediaStreamSource()\n * directly, which handles cross-browser compatibility internally.\n */\n\nimport { getContext } from 'tone';\n\n// Map of MediaStream -> MediaStreamAudioSourceNode\nconst streamSources = new Map<MediaStream, MediaStreamAudioSourceNode>();\n\n// Map of MediaStream -> cleanup handlers\nconst streamCleanupHandlers = new Map<MediaStream, () => void>();\n\n/**\n * Get or create a MediaStreamAudioSourceNode for the given stream\n *\n * @param stream - The MediaStream to create a source for\n * @returns MediaStreamAudioSourceNode that can be connected to multiple nodes\n *\n * @example\n * ```typescript\n * const source = getMediaStreamSource(stream);\n *\n * // Multiple consumers can connect to the same source\n * source.connect(analyserNode); // For VU meter\n * source.connect(workletNode); // For recording\n * ```\n */\nexport function getMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode {\n // Return existing source if we have one for this stream\n if (streamSources.has(stream)) {\n return streamSources.get(stream)!;\n }\n\n // Create new source using Tone.js's shared context for cross-browser compatibility\n const context = getContext();\n const source = context.createMediaStreamSource(stream);\n streamSources.set(stream, source);\n\n // Set up cleanup when stream ends\n const cleanup = () => {\n source.disconnect();\n streamSources.delete(stream);\n streamCleanupHandlers.delete(stream);\n\n // Remove event listener\n stream.removeEventListener('ended', cleanup);\n stream.removeEventListener('inactive', cleanup);\n };\n\n streamCleanupHandlers.set(stream, cleanup);\n\n // Clean up when stream ends or becomes inactive\n stream.addEventListener('ended', cleanup);\n stream.addEventListener('inactive', cleanup);\n\n return source;\n}\n\n/**\n * Manually release a MediaStreamSource\n *\n * Normally you don't need to call this - cleanup happens automatically\n * when the stream ends. Only call this if you need to force cleanup.\n *\n * @param stream - The MediaStream to release the source for\n */\nexport function releaseMediaStreamSource(stream: MediaStream): void {\n const cleanup = streamCleanupHandlers.get(stream);\n if (cleanup) {\n cleanup();\n }\n}\n\n/**\n * Check if a MediaStreamSource exists for the given stream\n *\n * @param stream - The MediaStream to check\n * @returns true if a source exists for this stream\n */\nexport function hasMediaStreamSource(stream: MediaStream): boolean {\n return streamSources.has(stream);\n}\n","import type { ClipTrack, Track } from '@waveform-playlist/core';\nimport {\n clipStartTime,\n clipEndTime,\n clipOffsetTime,\n clipDurationTime,\n} from '@waveform-playlist/core';\nimport type { PlayoutAdapter } from '@waveform-playlist/engine';\nimport { TonePlayout } from './TonePlayout';\nimport type { EffectsFunction } from './TonePlayout';\nimport type { ClipInfo } from './ToneTrack';\nimport type { MidiClipInfo } from './MidiToneTrack';\nimport type { SoundFontCache } from './SoundFontCache';\nimport { now } from 'tone';\n\nexport interface ToneAdapterOptions {\n effects?: EffectsFunction;\n /** When provided, MIDI clips use SoundFont sample playback instead of PolySynth */\n soundFontCache?: SoundFontCache;\n}\n\nexport function createToneAdapter(options?: ToneAdapterOptions): PlayoutAdapter {\n let playout: TonePlayout | null = null;\n let _isPlaying = false;\n let _playoutGeneration = 0;\n let _loopEnabled = false;\n let _loopStart = 0;\n let _loopEnd = 0;\n let _audioInitialized = false;\n\n // Add a single ClipTrack to the playout (shared by buildPlayout and addTrack)\n function addTrackToPlayout(p: TonePlayout, track: ClipTrack): void {\n const audioClips = track.clips.filter((c) => c.audioBuffer && !c.midiNotes);\n const midiClips = track.clips.filter((c) => c.midiNotes && c.midiNotes.length > 0);\n\n if (audioClips.length > 0) {\n const startTime = Math.min(...audioClips.map(clipStartTime));\n const endTime = Math.max(...audioClips.map(clipEndTime));\n\n const trackObj: Track = {\n id: track.id,\n name: track.name,\n gain: track.volume,\n muted: track.muted,\n soloed: track.soloed,\n stereoPan: track.pan,\n startTime,\n endTime,\n };\n\n const clipInfos: ClipInfo[] = audioClips.map((clip) => ({\n buffer: clip.audioBuffer!,\n startTime: clipStartTime(clip) - startTime,\n duration: clipDurationTime(clip),\n offset: clipOffsetTime(clip),\n fadeIn: clip.fadeIn,\n fadeOut: clip.fadeOut,\n gain: clip.gain,\n }));\n\n p.addTrack({\n clips: clipInfos,\n track: trackObj,\n effects: track.effects,\n });\n }\n\n if (midiClips.length > 0) {\n const startTime = Math.min(...midiClips.map(clipStartTime));\n const endTime = Math.max(...midiClips.map(clipEndTime));\n\n const trackId = audioClips.length > 0 ? `${track.id}:midi` : track.id;\n\n const trackObj: Track = {\n id: trackId,\n name: track.name,\n gain: track.volume,\n muted: track.muted,\n soloed: track.soloed,\n stereoPan: track.pan,\n startTime,\n endTime,\n };\n\n const midiClipInfos: MidiClipInfo[] = midiClips.map((clip) => ({\n notes: clip.midiNotes!,\n startTime: clipStartTime(clip) - startTime,\n duration: clipDurationTime(clip),\n offset: clipOffsetTime(clip),\n }));\n\n if (options?.soundFontCache?.isLoaded) {\n const firstClip = midiClips[0];\n const midiChannel = firstClip.midiChannel;\n const isPercussion = midiChannel === 9;\n const programNumber = firstClip.midiProgram ?? 0;\n\n p.addSoundFontTrack({\n clips: midiClipInfos,\n track: trackObj,\n soundFontCache: options.soundFontCache,\n programNumber,\n isPercussion,\n effects: track.effects,\n });\n } else {\n if (options?.soundFontCache) {\n console.warn(\n `[waveform-playlist] SoundFont not loaded for track \"${track.name}\" — falling back to PolySynth.`\n );\n }\n p.addMidiTrack({\n clips: midiClipInfos,\n track: trackObj,\n effects: track.effects,\n });\n }\n }\n }\n\n function buildPlayout(tracks: ClipTrack[]): void {\n if (playout) {\n try {\n playout.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing previous playout during rebuild:', err);\n }\n playout = null;\n }\n\n _playoutGeneration++;\n const generation = _playoutGeneration;\n\n playout = new TonePlayout({\n effects: options?.effects,\n });\n\n // If Tone.start() was already called (AudioContext resumed), carry\n // initialization forward. Tone.start() is safe to call multiple times —\n // it resolves immediately if the AudioContext is already running.\n if (_audioInitialized) {\n playout.init().catch((err) => {\n console.warn(\n '[waveform-playlist] Failed to re-initialize playout after rebuild. ' +\n 'Audio playback will require another user gesture.',\n err\n );\n _audioInitialized = false;\n });\n }\n\n for (const track of tracks) {\n addTrackToPlayout(playout, track);\n }\n playout.applyInitialSoloState();\n playout.setLoop(_loopEnabled, _loopStart, _loopEnd);\n\n playout.setOnPlaybackComplete(() => {\n if (generation === _playoutGeneration) {\n _isPlaying = false;\n }\n });\n }\n\n return {\n async init(): Promise<void> {\n if (playout) {\n await playout.init();\n _audioInitialized = true;\n }\n },\n\n setTracks(tracks: ClipTrack[]): void {\n buildPlayout(tracks);\n },\n\n addTrack(track: ClipTrack): void {\n if (!playout) {\n throw new Error(\n '[waveform-playlist] adapter.addTrack() called but no playout exists. ' +\n 'Call setTracks() first to initialize the playout.'\n );\n }\n addTrackToPlayout(playout, track);\n playout.applyInitialSoloState();\n },\n\n removeTrack(trackId: string): void {\n if (!playout) return;\n playout.removeTrack(trackId);\n playout.applyInitialSoloState();\n },\n\n play(startTime: number, endTime?: number): void {\n if (!playout) {\n console.warn(\n '[waveform-playlist] adapter.play() called but no playout is available. ' +\n 'Tracks may not have been set, or the adapter was disposed.'\n );\n return;\n }\n const duration = endTime !== undefined ? endTime - startTime : undefined;\n playout.play(now(), startTime, duration);\n // Only set _isPlaying if play() didn't throw\n // (TonePlayout.play() re-throws after cleanup on Transport failure)\n _isPlaying = true;\n },\n\n pause(): void {\n playout?.pause();\n _isPlaying = false;\n },\n\n stop(): void {\n playout?.stop();\n _isPlaying = false;\n },\n\n seek(time: number): void {\n playout?.seekTo(time);\n },\n\n getCurrentTime(): number {\n return playout?.getCurrentTime() ?? 0;\n },\n\n isPlaying(): boolean {\n return _isPlaying;\n },\n\n setMasterVolume(volume: number): void {\n playout?.setMasterGain(volume);\n },\n\n setTrackVolume(trackId: string, volume: number): void {\n playout?.getTrack(trackId)?.setVolume(volume);\n },\n\n setTrackMute(trackId: string, muted: boolean): void {\n playout?.setMute(trackId, muted);\n },\n\n setTrackSolo(trackId: string, soloed: boolean): void {\n playout?.setSolo(trackId, soloed);\n },\n\n setTrackPan(trackId: string, pan: number): void {\n playout?.getTrack(trackId)?.setPan(pan);\n },\n\n setLoop(enabled: boolean, start: number, end: number): void {\n _loopEnabled = enabled;\n _loopStart = start;\n _loopEnd = end;\n playout?.setLoop(enabled, start, end);\n },\n\n dispose(): void {\n try {\n playout?.dispose();\n } catch (err) {\n console.warn('[waveform-playlist] Error disposing playout:', err);\n }\n playout = null;\n _isPlaying = false;\n },\n };\n}\n"],"mappings":";AAAA;AAAA,EACE,UAAAA;AAAA,EAEA,kBAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAAC;AAAA,EACA,cAAAC;AAAA,OAEK;;;ACTP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACFP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAiBP,IAAI,YAAY;AAET,SAAS,wBAAwB,QAAyC;AAC/E,QAAM,QAAS,OAAmC;AAClD,MAAI,CAAC,SAAS,CAAC,WAAW;AACxB,gBAAY;AACZ,YAAQ;AAAA,MACN;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AACT;;;ADDO,IAAM,YAAN,MAAgB;AAAA,EAgBrB,YAAY,SAA2B;AAdvC,SAAQ,gBAA4C,oBAAI,IAAI;AAY5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAQ,uBAAuB;AAG7B,SAAK,QAAQ,QAAQ;AAGrB,SAAK,aAAa,IAAI,OAAO,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAI9D,SAAK,UAAU,IAAI,OAAO,EAAE,KAAK,QAAQ,MAAM,WAAW,cAAc,EAAE,CAAC;AAC3E,SAAK,WAAW,IAAI,KAAK,QAAQ,MAAM,QAAQ,IAAI,CAAC;AAGpD,SAAK,WAAW,MAAM,KAAK,SAAS,KAAK,QAAQ;AAGjD,UAAM,cAAc,QAAQ,eAAe,eAAe;AAC1D,QAAI,QAAQ,SAAS;AACnB,YAAM,UAAU,QAAQ,QAAQ,KAAK,UAAU,aAAa,KAAK;AACjE,UAAI,SAAS;AACX,aAAK,iBAAiB;AAAA,MACxB;AAAA,IACF,OAAO;AACL,WAAK,SAAS,QAAQ,WAAW;AAAA,IACnC;AAGA,UAAM,YACJ,QAAQ,UACP,QAAQ,SACL;AAAA,MACE;AAAA,QACE,QAAQ,QAAQ;AAAA,QAChB,WAAW;AAAA,QACX,UAAU,QAAQ,OAAO;AAAA,QACzB,QAAQ;AAAA,QACR,QAAQ,QAAQ,MAAM;AAAA,QACtB,SAAS,QAAQ,MAAM;AAAA,QACvB,MAAM;AAAA,MACR;AAAA,IACF,IACA,CAAC;AAEP,UAAM,YAAY,aAAa;AAC/B,UAAM,aAAa,WAAW,EAAE;AAKhC,UAAM,oBAAqB,KAAK,WAAW,MAA0B;AAGrE,SAAK,iBAAiB,UAAU,IAAI,CAAC,aAAa;AAEhD,YAAM,eAAe,WAAW,WAAW;AAC3C,mBAAa,KAAK,QAAQ,SAAS;AACnC,mBAAa,QAAQ,iBAAiB;AAKtC,YAAM,mBAAmB,KAAK,MAAM,YAAY,SAAS;AACzD,YAAM,aAAa,UAAU,SAAS,CAAC,qBAA6B;AAIlE,YAAI,mBAAmB,KAAK,sBAAsB;AAChD;AAAA,QACF;AACA,aAAK,gBAAgB,UAAU,cAAc,gBAAgB;AAAA,MAC/D,GAAG,gBAAgB;AAEnB,aAAO,EAAE,UAAU,cAAc,WAAW;AAAA,IAC9C,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,gBACN,UACA,cACA,kBACA,cACA,cACM;AACN,UAAM,aAAa,WAAW,EAAE;AAChC,UAAM,SAAS,WAAW,mBAAmB;AAC7C,WAAO,SAAS,SAAS;AACzB,WAAO,QAAQ,YAAY;AAE3B,UAAM,SAAS,gBAAgB,SAAS;AACxC,UAAM,WAAW,gBAAgB,SAAS;AAE1C,QAAI;AACF,aAAO,MAAM,kBAAkB,QAAQ,QAAQ;AAAA,IACjD,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,wDAAwD,KAAK,EAAE,WACpD,gBAAgB,YAAY,MAAM,cAAc,QAAQ;AAAA,QACnE;AAAA,MACF;AACA,aAAO,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,cAAc,IAAI,MAAM;AAC7B,WAAO,UAAU,MAAM;AACrB,WAAK,cAAc,OAAO,MAAM;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,uBAAuB,QAAsB;AAC3C,SAAK,uBAAuB;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,oBAAoB,iBAAyB,kBAAgC;AAC3E,eAAW,EAAE,UAAU,aAAa,KAAK,KAAK,gBAAgB;AAC5D,YAAM,eAAe,KAAK,MAAM,YAAY,SAAS;AACrD,YAAM,aAAa,eAAe,SAAS;AAI3C,UAAI,eAAe,mBAAmB,aAAa,iBAAiB;AAClE,cAAM,UAAU,kBAAkB;AAClC,cAAM,iBAAiB,SAAS,SAAS;AACzC,cAAM,oBAAoB,SAAS,WAAW;AAC9C,aAAK;AAAA,UACH;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAuB;AACrB,SAAK,cAAc,QAAQ,CAAC,WAAW;AACrC,UAAI;AACF,eAAO,KAAK;AAAA,MACd,SAAS,KAAK;AACZ,gBAAQ,KAAK,uDAAuD,KAAK,EAAE,MAAM,GAAG;AAAA,MACtF;AAAA,IACF,CAAC;AACD,SAAK,cAAc,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cACN,WACAC,gBACA,aAAqB,GACf;AACN,UAAM,EAAE,UAAU,aAAa,IAAI;AACnC,UAAM,aAAa,aAAa;AAGhC,eAAW,sBAAsB,CAAC;AAGlC,UAAM,WAAW,aAAa,SAAS;AAGvC,QAAI,SAAS,UAAU,WAAW,SAAS,OAAO,UAAU;AAC1D,YAAM,iBAAiB,SAAS,OAAO;AAEvC,UAAI,YAAY,GAAG;AACjB;AAAA,UACE;AAAA,UACAA;AAAA,UACA;AAAA,UACA,SAAS,OAAO,QAAQ;AAAA,UACxB;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF,OAAO;AACL,cAAM,wBAAwB,iBAAiB;AAC/C,cAAM,eAAe,WAAW;AAChC,cAAM,aAAa,SAAS,OAAO;AACnC;AAAA,UACE;AAAA,UACAA;AAAA,UACA;AAAA,UACA,SAAS,OAAO,QAAQ;AAAA,UACxB;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF,OAAO;AACL,iBAAW,eAAe,SAAS,MAAMA,cAAa;AAAA,IACxD;AAGA,QAAI,SAAS,SAAS;AACpB,YAAM,eAAe,SAAS,WAAW,SAAS,QAAQ;AAC1D,YAAM,qBAAqB,eAAe;AAE1C,UAAI,qBAAqB,GAAG;AAC1B,cAAM,uBAAuBA,iBAAgB;AAC7C;AAAA,UACE;AAAA,UACA;AAAA,UACA,SAAS,QAAQ;AAAA,UACjB,SAAS,QAAQ,QAAQ;AAAA,UACzB,SAAS;AAAA,UACT;AAAA,QACF;AAAA,MACF,WAAW,qBAAqB,CAAC,SAAS,QAAQ,UAAU;AAC1D,cAAM,iBAAiB,CAAC;AACxB,cAAM,wBAAwB,SAAS,QAAQ,WAAW;AAC1D,cAAM,eAAe,iBAAiB,SAAS,QAAQ;AACvD,cAAM,aAAa,SAAS,QAAQ,IAAI;AACxC;AAAA,UACE;AAAA,UACAA;AAAA,UACA;AAAA,UACA,SAAS,QAAQ,QAAQ;AAAA,UACzB;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,MAAc,iBAA+B;AACxD,SAAK,eAAe,QAAQ,CAAC,cAAc;AACzC,YAAM,eAAe,KAAK,MAAM,YAAY,UAAU,SAAS;AAC/D,YAAM,aAAa,eAAe,UAAU,SAAS;AAErD,UAAI,mBAAmB,WAAY;AAEnC,UAAI,mBAAmB,cAAc;AAEnC,cAAM,aAAa,kBAAkB,eAAe,UAAU,SAAS;AACvE,aAAK,cAAc,WAAW,MAAM,UAAU;AAAA,MAChD,OAAO;AAEL,cAAM,QAAQ,eAAe;AAC7B,aAAK,cAAc,WAAW,OAAO,OAAO,UAAU,SAAS,MAAM;AAAA,MACvE;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAoB;AAClB,SAAK,eAAe,QAAQ,CAAC,EAAE,cAAc,SAAS,MAAM;AAC1D,YAAM,aAAa,aAAa;AAChC,iBAAW,sBAAsB,CAAC;AAClC,iBAAW,eAAe,SAAS,MAAM,CAAC;AAAA,IAC5C,CAAC;AAAA,EACH;AAAA,EAEQ,SAAS,MAAsB;AACrC,WAAO,KAAK,KAAK,MAAM,IAAI;AAAA,EAC7B;AAAA,EAEA,UAAU,MAAoB;AAC5B,SAAK,MAAM,OAAO;AAClB,SAAK,WAAW,OAAO,QAAQ,KAAK,SAAS,IAAI;AAAA,EACnD;AAAA,EAEA,OAAO,KAAmB;AACxB,SAAK,MAAM,YAAY;AACvB,SAAK,QAAQ,IAAI,QAAQ;AAAA,EAC3B;AAAA,EAEA,QAAQ,OAAsB;AAC5B,SAAK,MAAM,QAAQ;AACnB,UAAM,QAAQ,QAAQ,IAAI;AAM1B,UAAM,aAAa,wBAAwB,KAAK,SAAS,IAAI;AAC7D,gBAAY,eAAe,OAAO,CAAC;AACnC,SAAK,SAAS,KAAK,QAAQ;AAAA,EAC7B;AAAA,EAEA,QAAQ,QAAuB;AAC7B,SAAK,MAAM,SAAS;AAAA,EACtB;AAAA,EAEA,UAAgB;AACd,UAAM,YAAY,aAAa;AAE/B,QAAI,KAAK,gBAAgB;AACvB,UAAI;AACF,aAAK,eAAe;AAAA,MACtB,SAAS,KAAK;AACZ,gBAAQ,KAAK,2CAA2C,KAAK,EAAE,sBAAsB,GAAG;AAAA,MAC1F;AAAA,IACF;AAEA,SAAK,eAAe;AAGpB,SAAK,eAAe,QAAQ,CAAC,WAAW,UAAU;AAChD,UAAI;AACF,kBAAU,MAAM,UAAU,UAAU;AAAA,MACtC,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,+CAA+C,KAAK,cAAc,KAAK,EAAE;AAAA,UACzE;AAAA,QACF;AAAA,MACF;AACA,UAAI;AACF,kBAAU,aAAa,WAAW;AAAA,MACpC,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,oDAAoD,KAAK,cAAc,KAAK,EAAE;AAAA,UAC9E;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAED,QAAI;AACF,WAAK,WAAW,QAAQ;AAAA,IAC1B,SAAS,KAAK;AACZ,cAAQ,KAAK,4DAA4D,KAAK,EAAE,MAAM,GAAG;AAAA,IAC3F;AACA,QAAI;AACF,WAAK,QAAQ,QAAQ;AAAA,IACvB,SAAS,KAAK;AACZ,cAAQ,KAAK,yDAAyD,KAAK,EAAE,MAAM,GAAG;AAAA,IACxF;AACA,QAAI;AACF,WAAK,SAAS,QAAQ;AAAA,IACxB,SAAS,KAAK;AACZ,cAAQ,KAAK,0DAA0D,KAAK,EAAE,MAAM,GAAG;AAAA,IACzF;AAAA,EACF;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,IAAI,WAAmB;AACrB,QAAI,KAAK,eAAe,WAAW,EAAG,QAAO;AAC7C,UAAM,WAAW,KAAK,eAAe,KAAK,eAAe,SAAS,CAAC;AACnE,WAAO,SAAS,SAAS,YAAY,SAAS,SAAS;AAAA,EACzD;AAAA,EAEA,IAAI,SAAsB;AACxB,WAAO,KAAK,eAAe,CAAC,GAAG,SAAS;AAAA,EAC1C;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK,MAAM;AAAA,EACpB;AACF;;;AE3bA;AAAA,EACE,UAAAC;AAAA,EACA,QAAAC;AAAA,EACA,UAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,kBAAAC;AAAA,EACA,cAAAC;AAAA,OACK;AAkDP,SAAS,gBAAgB,UAAgC;AAEvD,MAAI,aAAa,MAAM,aAAa,GAAI,QAAO;AAE/C,MAAI,YAAY,MAAM,YAAY,GAAI,QAAO;AAE7C,MACE,aAAa,MACb,aAAa,MACb,aAAa,MACb,aAAa,MACb,aAAa,MACb,aAAa;AAEb,WAAO;AAET,SAAO;AACT;AAcO,IAAM,gBAAN,MAA6C;AAAA,EAelD,YAAY,SAA+B;AACzC,SAAK,QAAQ,QAAQ;AAGrB,SAAK,aAAa,IAAIC,QAAO,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAC9D,SAAK,UAAU,IAAIC,QAAO,QAAQ,MAAM,SAAS;AACjD,SAAK,WAAW,IAAIC,MAAK,QAAQ,MAAM,QAAQ,IAAI,CAAC;AACpD,SAAK,WAAW,MAAM,KAAK,SAAS,KAAK,QAAQ;AAGjD,SAAK,QAAQ,IAAI,UAAU,OAAO,QAAQ,YAAY;AACtD,SAAK,MAAM,QAAQ,KAAK,UAAU;AAGlC,SAAK,YAAY,IAAI,UAAU,eAAe;AAAA,MAC5C,OAAO;AAAA,MACP,SAAS;AAAA,QACP,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,UAAU,EAAE,QAAQ,MAAO,OAAO,KAAK,SAAS,GAAG,SAAS,IAAI;AAAA,MAClE;AAAA,IACF,CAAU;AACV,SAAK,aAAa,IAAI,WAAW;AAAA,MAC/B,OAAO,EAAE,MAAM,QAAQ;AAAA,MACvB,UAAU,EAAE,QAAQ,MAAO,OAAO,MAAM,SAAS,GAAG,SAAS,KAAK;AAAA,IACpE,CAAC;AACD,SAAK,cAAc,IAAI,UAAU,YAAY;AAAA,MAC3C,OAAO;AAAA,MACP,SAAS;AAAA,QACP,UAAU,EAAE,QAAQ,MAAO,OAAO,KAAK,SAAS,IAAI;AAAA,QACpD,aAAa;AAAA,QACb,iBAAiB;AAAA,QACjB,WAAW;AAAA,QACX,SAAS;AAAA,MACX;AAAA,IACF,CAAU;AACV,SAAK,WAAW,IAAI,UAAU,eAAe;AAAA,MAC3C,OAAO;AAAA,MACP,SAAS;AAAA,QACP,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,UAAU,EAAE,QAAQ,MAAO,OAAO,KAAK,SAAS,GAAG,SAAS,IAAI;AAAA,MAClE;AAAA,IACF,CAAU;AAEV,SAAK,UAAU,QAAQ,KAAK,UAAU;AACtC,SAAK,WAAW,QAAQ,KAAK,UAAU;AACvC,SAAK,YAAY,QAAQ,KAAK,UAAU;AACxC,SAAK,SAAS,QAAQ,KAAK,UAAU;AAGrC,UAAM,cAAc,QAAQ,eAAeC,gBAAe;AAC1D,QAAI,QAAQ,SAAS;AACnB,YAAM,UAAU,QAAQ,QAAQ,KAAK,UAAU,aAAa,KAAK;AACjE,UAAI,SAAS;AACX,aAAK,iBAAiB;AAAA,MACxB;AAAA,IACF,OAAO;AACL,WAAK,SAAS,QAAQ,WAAW;AAAA,IACnC;AAGA,SAAK,iBAAiB,QAAQ,MAAM,IAAI,CAAC,aAAa;AAEpD,YAAM,eAAe,SAAS,MAAM,OAAO,CAAC,SAAS;AACnD,cAAM,UAAU,KAAK,OAAO,KAAK;AAEjC,eAAO,KAAK,OAAO,SAAS,SAAS,SAAS,YAAY,UAAU,SAAS;AAAA,MAC/E,CAAC;AAGD,YAAM,eAAe,KAAK,MAAM,YAAY,SAAS;AAErD,YAAM,aAAa,aAAa,IAAI,CAAC,SAAS;AAE5C,cAAM,eAAe,KAAK,OAAO,SAAS;AAE1C,cAAM,eAAe,KAAK,IAAI,GAAG,YAAY;AAC7C,cAAM,kBAAkB,KAAK;AAAA,UAC3B,KAAK,WAAW,KAAK,IAAI,GAAG,SAAS,SAAS,KAAK,IAAI;AAAA,UACvD,SAAS,WAAW;AAAA,QACtB;AAEA,eAAO;AAAA,UACL,MAAM,eAAe;AAAA,UACrB,MAAM,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,UACX,UAAU,KAAK,IAAI,GAAG,eAAe;AAAA,UACrC,UAAU,KAAK;AAAA,UACf,SAAS,KAAK;AAAA,QAChB;AAAA,MACF,CAAC;AAED,YAAM,OAAO,IAAI,KAAK,CAAC,MAAM,UAAU;AACrC,YAAI,MAAM,WAAW,GAAG;AACtB,eAAK;AAAA,YACH,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,YACN;AAAA,YACA,MAAM;AAAA,YACN,MAAM;AAAA,UACR;AAAA,QACF;AAAA,MACF,GAAG,UAAU;AAGb,WAAK,MAAM,CAAC;AAEZ,aAAO,EAAE,UAAU,KAAK;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,YACN,UACA,UACA,UACA,MACA,UACA,SACM;AACN,QAAI,YAAY,GAAG;AACjB,YAAM,WAAW,gBAAgB,QAAQ;AACzC,cAAQ,UAAU;AAAA,QAChB,KAAK;AACH,eAAK,UAAU,qBAAqB,MAAM,UAAU,MAAM,QAAQ;AAClE;AAAA,QACF,KAAK;AAEH,cAAI;AACF,iBAAK,WAAW,qBAAqB,UAAU,MAAM,QAAQ;AAAA,UAC/D,SAAS,KAAK;AACZ,oBAAQ;AAAA,cACN;AAAA,cACA;AAAA,YACF;AAAA,UACF;AACA;AAAA,QACF,KAAK,OAAO;AACV,gBAAM,aAAqC;AAAA,YACzC,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,IAAI;AAAA,UACN;AACA,eAAK,SAAS;AAAA,YACZ,WAAW,QAAQ,KAAK;AAAA,YACxB;AAAA,YACA;AAAA,YACA;AAAA,UACF;AACA;AAAA,QACF;AAAA,QACA,KAAK;AAGH,eAAK,YAAY,qBAAqB,MAAM,UAAU,MAAM,QAAQ;AACpE;AAAA,MACJ;AAAA,IACF,OAAO;AACL,WAAK,MAAM,qBAAqB,UAAU,UAAU,MAAM,QAAQ;AAAA,IACpE;AAAA,EACF;AAAA,EAEQ,SAAS,MAAsB;AACrC,WAAO,KAAK,KAAK,MAAM,IAAI;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,uBAAuB,SAAuB;AAAA,EAE9C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,oBAAoB,iBAAyB,kBAAgC;AAC3E,eAAW,EAAE,SAAS,KAAK,KAAK,gBAAgB;AAC9C,YAAM,eAAe,KAAK,MAAM,YAAY,SAAS;AACrD,YAAM,aAAa,eAAe,SAAS;AAE3C,UAAI,eAAe,mBAAmB,aAAa,iBAAiB;AAElE,mBAAW,QAAQ,SAAS,OAAO;AACjC,gBAAM,eAAe,KAAK,OAAO,SAAS;AAC1C,gBAAM,eAAe,eAAe,KAAK,IAAI,GAAG,YAAY;AAC5D,gBAAM,aAAa,eAAe,KAAK;AAEvC,cAAI,eAAe,mBAAmB,aAAa,iBAAiB;AAClE,kBAAM,oBAAoB,aAAa;AACvC,gBAAI;AACF,mBAAK;AAAA,gBACH,KAAK;AAAA,gBACL,KAAK;AAAA,gBACL;AAAA,gBACA;AAAA,gBACA,KAAK;AAAA,gBACL,KAAK;AAAA,cACP;AAAA,YACF,SAAS,KAAK;AACZ,sBAAQ;AAAA,gBACN,2DAA2D,KAAK,IAAI,eAAe,KAAK,EAAE;AAAA,gBAC1F;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAuB;AACrB,UAAMC,OAAMC,YAAW,EAAE,WAAW;AACpC,QAAI;AACF,WAAK,MAAM,WAAWD,IAAG;AACzB,WAAK,UAAU,WAAWA,IAAG;AAC7B,WAAK,YAAY,WAAWA,IAAG;AAC/B,WAAK,SAAS,WAAWA,IAAG;AAAA,IAE9B,SAAS,KAAK;AACZ,cAAQ,KAAK,uDAAuD,KAAK,EAAE,MAAM,GAAG;AAAA,IACtF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,OAAe,SAAuB;AAAA,EAEnD;AAAA;AAAA;AAAA;AAAA,EAKA,cAAoB;AAAA,EAEpB;AAAA,EAEA,UAAU,MAAoB;AAC5B,SAAK,MAAM,OAAO;AAClB,SAAK,WAAW,OAAO,QAAQ,KAAK,SAAS,IAAI;AAAA,EACnD;AAAA,EAEA,OAAO,KAAmB;AACxB,SAAK,MAAM,YAAY;AACvB,SAAK,QAAQ,IAAI,QAAQ;AAAA,EAC3B;AAAA,EAEA,QAAQ,OAAsB;AAC5B,SAAK,MAAM,QAAQ;AACnB,UAAM,QAAQ,QAAQ,IAAI;AAC1B,UAAM,aAAa,wBAAwB,KAAK,SAAS,IAAI;AAC7D,gBAAY,eAAe,OAAO,CAAC;AACnC,SAAK,SAAS,KAAK,QAAQ;AAAA,EAC7B;AAAA,EAEA,QAAQ,QAAuB;AAC7B,SAAK,MAAM,SAAS;AAAA,EACtB;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK,gBAAgB;AACvB,UAAI;AACF,aAAK,eAAe;AAAA,MACtB,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,gDAAgD,KAAK,EAAE;AAAA,UACvD;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,SAAK,eAAe;AAGpB,SAAK,eAAe,QAAQ,CAAC,EAAE,KAAK,GAAG,UAAU;AAC/C,UAAI;AACF,aAAK,QAAQ;AAAA,MACf,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,4CAA4C,KAAK,mBAAmB,KAAK,EAAE;AAAA,UAC3E;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAGD,UAAM,kBAAkB;AAAA,MACtB,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,IACP;AACA,eAAW,KAAK,iBAAiB;AAC/B,UAAI;AACF,WAAG,QAAQ;AAAA,MACb,SAAS,KAAK;AACZ,gBAAQ,KAAK,4DAA4D,KAAK,EAAE,MAAM,GAAG;AAAA,MAC3F;AAAA,IACF;AACA,QAAI;AACF,WAAK,WAAW,QAAQ;AAAA,IAC1B,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,iEAAiE,KAAK,EAAE;AAAA,QACxE;AAAA,MACF;AAAA,IACF;AACA,QAAI;AACF,WAAK,QAAQ,QAAQ;AAAA,IACvB,SAAS,KAAK;AACZ,cAAQ,KAAK,8DAA8D,KAAK,EAAE,MAAM,GAAG;AAAA,IAC7F;AACA,QAAI;AACF,WAAK,SAAS,QAAQ;AAAA,IACxB,SAAS,KAAK;AACZ,cAAQ,KAAK,+DAA+D,KAAK,EAAE,MAAM,GAAG;AAAA,IAC9F;AAAA,EACF;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,IAAI,WAAmB;AACrB,QAAI,KAAK,eAAe,WAAW,EAAG,QAAO;AAC7C,UAAM,WAAW,KAAK,eAAe,KAAK,eAAe,SAAS,CAAC;AACnE,WAAO,SAAS,SAAS,YAAY,SAAS,SAAS;AAAA,EACzD;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK,MAAM;AAAA,EACpB;AACF;;;AC5cA,SAAS,UAAAE,SAAQ,QAAAC,OAAM,UAAAC,SAAQ,QAAAC,OAAqB,kBAAAC,iBAAgB,cAAAC,mBAAkB;AAqC/E,IAAM,sBAAN,MAAM,oBAA4C;AAAA,EAcvD,YAAY,SAAoC;AAVhD,SAAQ,gBAA4C,oBAAI,IAAI;AAW1D,SAAK,QAAQ,QAAQ;AACrB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,gBAAgB,QAAQ,iBAAiB;AAE9C,SAAK,aAAa,QAAQ,eAAe,MAAM;AAG/C,SAAK,aAAa,IAAIC,QAAO,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAC9D,SAAK,UAAU,IAAIC,QAAO,QAAQ,MAAM,SAAS;AACjD,SAAK,WAAW,IAAIC,MAAK,QAAQ,MAAM,QAAQ,IAAI,CAAC;AACpD,SAAK,WAAW,MAAM,KAAK,SAAS,KAAK,QAAQ;AAGjD,UAAM,cAAc,QAAQ,eAAeC,gBAAe;AAC1D,QAAI,QAAQ,SAAS;AACnB,YAAM,UAAU,QAAQ,QAAQ,KAAK,UAAU,aAAa,KAAK;AACjE,UAAI,SAAS;AACX,aAAK,iBAAiB;AAAA,MACxB;AAAA,IACF,OAAO;AACL,WAAK,SAAS,QAAQ,WAAW;AAAA,IACnC;AAGA,SAAK,iBAAiB,QAAQ,MAAM,IAAI,CAAC,aAAa;AAEpD,YAAM,eAAe,SAAS,MAAM,OAAO,CAAC,SAAS;AACnD,cAAM,UAAU,KAAK,OAAO,KAAK;AACjC,eAAO,KAAK,OAAO,SAAS,SAAS,SAAS,YAAY,UAAU,SAAS;AAAA,MAC/E,CAAC;AAED,YAAM,eAAe,KAAK,MAAM,YAAY,SAAS;AAErD,YAAM,aAAa,aAAa,IAAI,CAAC,SAAS;AAC5C,cAAM,eAAe,KAAK,OAAO,SAAS;AAC1C,cAAM,eAAe,KAAK,IAAI,GAAG,YAAY;AAC7C,cAAM,kBAAkB,KAAK;AAAA,UAC3B,KAAK,WAAW,KAAK,IAAI,GAAG,SAAS,SAAS,KAAK,IAAI;AAAA,UACvD,SAAS,WAAW;AAAA,QACtB;AAEA,eAAO;AAAA,UACL,MAAM,eAAe;AAAA,UACrB,MAAM,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,UACX,UAAU,KAAK,IAAI,GAAG,eAAe;AAAA,UACrC,UAAU,KAAK;AAAA,UACf,SAAS,KAAK;AAAA,QAChB;AAAA,MACF,CAAC;AAED,YAAM,OAAO,IAAIC,MAAK,CAAC,MAAM,UAAU;AACrC,YAAI,MAAM,WAAW,GAAG;AACtB,eAAK,YAAY,MAAM,MAAM,MAAM,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO;AAAA,QAClF;AAAA,MACF,GAAG,UAAU;AAEb,WAAK,MAAM,CAAC;AAEZ,aAAO,EAAE,UAAU,KAAK;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,YACN,UACA,UACA,MACA,UACA,SACM;AACN,UAAM,OAAO,YAAY,IAAI,MAAM,KAAK;AACxC,UAAM,SAAS,YAAY,IAAI,IAAI,KAAK;AAExC,UAAM,WAAW,KAAK,eAAe,eAAe,UAAU,MAAM,MAAM;AAC1E,QAAI,CAAC,UAAU;AACb,UAAI,CAAC,oBAAmB,sBAAsB;AAC5C,gBAAQ;AAAA,UACN,gEAAgE,QAAQ,UAAU,IAAI,YAAY,MAAM;AAAA,QAE1G;AACA,4BAAmB,uBAAuB;AAAA,MAC5C;AACA;AAAA,IACF;AAEA,UAAM,aAAaC,YAAW,EAAE;AAGhC,UAAM,SAAS,WAAW,mBAAmB;AAC7C,WAAO,SAAS,SAAS;AACzB,WAAO,aAAa,QAAQ,SAAS;AAGrC,QAAI,SAAS,aAAa,KAAK,SAAS,aAAa,GAAG;AACtD,aAAO,OAAO;AACd,aAAO,YAAY,SAAS;AAC5B,aAAO,UAAU,SAAS;AAAA,IAC5B;AAKA,UAAM,iBAAiB,SAAS,OAAO,WAAW,SAAS;AAC3D,UAAM,oBACJ,SAAS,aAAa,IAAI,KAAK,IAAI,UAAU,cAAc,IAAI;AAGjE,UAAM,WAAW,WAAW;AAC5B,UAAM,WAAW,WAAW,WAAW;AACvC,UAAM,EAAE,cAAc,YAAY,aAAa,eAAe,cAAc,IAAI;AAChF,UAAM,cAAc,WAAW;AAG/B,aAAS,KAAK,eAAe,GAAG,IAAI;AACpC,aAAS,KAAK,wBAAwB,UAAU,OAAO,YAAY;AAEnE,QAAI,aAAa,MAAO;AACtB,eAAS,KAAK,eAAe,UAAU,OAAO,eAAe,UAAU;AAAA,IACzE;AAEA,UAAM,aAAa,OAAO,eAAe;AACzC,aAAS,KAAK,wBAAwB,aAAa,aAAa,WAAW;AAI3E,aAAS,KAAK,eAAe,aAAa,OAAO,iBAAiB;AAClE,aAAS,KAAK,wBAAwB,GAAG,OAAO,oBAAoB,aAAa;AAGjF,WAAO,QAAQ,QAAQ;AACvB,aAAS,QAAS,KAAK,WAAW,MAA0B,KAAK;AAGjE,SAAK,cAAc,IAAI,MAAM;AAC7B,WAAO,UAAU,MAAM;AACrB,WAAK,cAAc,OAAO,MAAM;AAChC,UAAI;AACF,iBAAS,WAAW;AAAA,MACtB,SAAS,KAAK;AACZ,gBAAQ,KAAK,sDAAsD,GAAG;AAAA,MACxE;AAAA,IACF;AAEA,WAAO,MAAM,IAAI;AACjB,WAAO,KAAK,OAAO,oBAAoB,aAAa;AAAA,EACtD;AAAA,EAEQ,SAAS,MAAsB;AACrC,WAAO,KAAK,KAAK,MAAM,IAAI;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,uBAAuB,SAAuB;AAAA,EAE9C;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAoB,iBAAyB,kBAAgC;AAC3E,eAAW,EAAE,SAAS,KAAK,KAAK,gBAAgB;AAC9C,YAAM,eAAe,KAAK,MAAM,YAAY,SAAS;AACrD,YAAM,aAAa,eAAe,SAAS;AAE3C,UAAI,eAAe,mBAAmB,aAAa,iBAAiB;AAClE,mBAAW,QAAQ,SAAS,OAAO;AACjC,gBAAM,eAAe,KAAK,OAAO,SAAS;AAC1C,gBAAM,eAAe,eAAe,KAAK,IAAI,GAAG,YAAY;AAC5D,gBAAM,aAAa,eAAe,KAAK;AAEvC,cAAI,eAAe,mBAAmB,aAAa,iBAAiB;AAClE,kBAAM,oBAAoB,aAAa;AACvC,gBAAI;AACF,mBAAK;AAAA,gBACH,KAAK;AAAA,gBACL;AAAA,gBACA;AAAA,gBACA,KAAK;AAAA,gBACL,KAAK;AAAA,cACP;AAAA,YACF,SAAS,KAAK;AACZ,sBAAQ;AAAA,gBACN,yEAAyE,KAAK,EAAE;AAAA,gBAChF;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAuB;AACrB,eAAW,UAAU,KAAK,eAAe;AACvC,UAAI;AACF,eAAO,KAAK;AAAA,MACd,SAAS,KAAK;AACZ,gBAAQ,KAAK,6DAA6D,GAAG;AAAA,MAC/E;AAAA,IACF;AACA,SAAK,cAAc,MAAM;AAAA,EAC3B;AAAA;AAAA,EAGA,aAAa,OAAe,SAAuB;AAAA,EAAC;AAAA;AAAA,EAGpD,cAAoB;AAAA,EAAC;AAAA,EAErB,UAAU,MAAoB;AAC5B,SAAK,MAAM,OAAO;AAClB,SAAK,WAAW,OAAO,QAAQ,KAAK,SAAS,IAAI;AAAA,EACnD;AAAA,EAEA,OAAO,KAAmB;AACxB,SAAK,MAAM,YAAY;AACvB,SAAK,QAAQ,IAAI,QAAQ;AAAA,EAC3B;AAAA,EAEA,QAAQ,OAAsB;AAC5B,SAAK,MAAM,QAAQ;AACnB,UAAM,QAAQ,QAAQ,IAAI;AAC1B,UAAM,aAAa,wBAAwB,KAAK,SAAS,IAAI;AAC7D,gBAAY,eAAe,OAAO,CAAC;AACnC,SAAK,SAAS,KAAK,QAAQ;AAAA,EAC7B;AAAA,EAEA,QAAQ,QAAuB;AAC7B,SAAK,MAAM,SAAS;AAAA,EACtB;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK,gBAAgB;AACvB,UAAI;AACF,aAAK,eAAe;AAAA,MACtB,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,qDAAqD,KAAK,EAAE;AAAA,UAC5D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,SAAK,eAAe;AAGpB,SAAK,eAAe,QAAQ,CAAC,EAAE,KAAK,GAAG,UAAU;AAC/C,UAAI;AACF,aAAK,QAAQ;AAAA,MACf,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,4CAA4C,KAAK,wBAAwB,KAAK,EAAE;AAAA,UAChF;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAED,QAAI;AACF,WAAK,WAAW,QAAQ;AAAA,IAC1B,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,sEAAsE,KAAK,EAAE;AAAA,QAC7E;AAAA,MACF;AAAA,IACF;AACA,QAAI;AACF,WAAK,QAAQ,QAAQ;AAAA,IACvB,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,mEAAmE,KAAK,EAAE;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AACA,QAAI;AACF,WAAK,SAAS,QAAQ;AAAA,IACxB,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,oEAAoE,KAAK,EAAE;AAAA,QAC3E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,IAAI,WAAmB;AACrB,QAAI,KAAK,eAAe,WAAW,EAAG,QAAO;AAC7C,UAAM,WAAW,KAAK,eAAe,KAAK,eAAe,SAAS,CAAC;AACnE,WAAO,SAAS,SAAS,YAAY,SAAS,SAAS;AAAA,EACzD;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK,MAAM;AAAA,EACpB;AACF;AAAA;AArUa,oBAEI,uBAAuB;AAFjC,IAAM,qBAAN;;;AJVA,IAAM,cAAN,MAAkB;AAAA,EAcvB,YAAY,UAA8B,CAAC,GAAG;AAb9C,SAAQ,SAAqC,oBAAI,IAAI;AAErD,SAAQ,gBAAgB;AACxB,SAAQ,eAA4B,oBAAI,IAAI;AAC5C,SAAQ,kBAAwC,oBAAI,IAAI;AAGxD,SAAQ,qBAAoC;AAC5C,SAAQ,eAAoC;AAC5C,SAAQ,eAAe;AACvB,SAAQ,aAAa;AACrB,SAAQ,WAAW;AAGjB,SAAK,eAAe,IAAIC,QAAO,KAAK,SAAS,QAAQ,cAAc,CAAC,CAAC;AAGrE,QAAI,QAAQ,SAAS;AACnB,YAAM,UAAU,QAAQ,QAAQ,KAAK,cAAcC,gBAAe,GAAG,KAAK;AAC1E,UAAI,SAAS;AACX,aAAK,iBAAiB;AAAA,MACxB;AAAA,IACF,OAAO;AACL,WAAK,aAAa,cAAc;AAAA,IAClC;AAEA,QAAI,QAAQ,QAAQ;AAClB,cAAQ,OAAO,QAAQ,CAAC,UAAU;AAChC,aAAK,OAAO,IAAI,MAAM,IAAI,KAAK;AAC/B,aAAK,gBAAgB,IAAI,MAAM,IAAI,MAAM,KAAK;AAAA,MAChD,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEQ,SAAS,MAAsB;AACrC,WAAO,KAAK,KAAK,MAAM,IAAI;AAAA,EAC7B;AAAA,EAEQ,uBAA6B;AACnC,QAAI,KAAK,uBAAuB,MAAM;AACpC,UAAI;AACF,QAAAC,cAAa,EAAE,MAAM,KAAK,kBAAkB;AAAA,MAC9C,SAAS,KAAK;AACZ,gBAAQ,KAAK,kEAAkE,GAAG;AAAA,MACpF;AACA,WAAK,qBAAqB;AAAA,IAC5B;AAAA,EACF;AAAA,EAEA,MAAM,OAAsB;AAC1B,QAAI,KAAK,cAAe;AAExB,UAAM,MAAM;AACZ,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,SAAS,cAA2C;AAElD,UAAM,yBAAyB;AAAA,MAC7B,GAAG;AAAA,MACH,aAAa,KAAK;AAAA,IACpB;AACA,UAAM,YAAY,IAAI,UAAU,sBAAsB;AACtD,SAAK,OAAO,IAAI,UAAU,IAAI,SAAS;AACvC,SAAK,gBAAgB,IAAI,UAAU,IAAI,aAAa,MAAM,SAAS,KAAK;AACxE,QAAI,aAAa,MAAM,QAAQ;AAC7B,WAAK,aAAa,IAAI,UAAU,EAAE;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,aAAa,cAAmD;AAC9D,UAAM,yBAAyB;AAAA,MAC7B,GAAG;AAAA,MACH,aAAa,KAAK;AAAA,IACpB;AACA,UAAM,YAAY,IAAI,cAAc,sBAAsB;AAC1D,SAAK,OAAO,IAAI,UAAU,IAAI,SAAS;AACvC,SAAK,gBAAgB,IAAI,UAAU,IAAI,aAAa,MAAM,SAAS,KAAK;AACxE,QAAI,aAAa,MAAM,QAAQ;AAC7B,WAAK,aAAa,IAAI,UAAU,EAAE;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,kBAAkB,cAA6D;AAC7E,UAAM,yBAAyB;AAAA,MAC7B,GAAG;AAAA,MACH,aAAa,KAAK;AAAA,IACpB;AACA,UAAM,UAAU,IAAI,mBAAmB,sBAAsB;AAC7D,SAAK,OAAO,IAAI,QAAQ,IAAI,OAAO;AACnC,SAAK,gBAAgB,IAAI,QAAQ,IAAI,aAAa,MAAM,SAAS,KAAK;AACtE,QAAI,aAAa,MAAM,QAAQ;AAC7B,WAAK,aAAa,IAAI,QAAQ,EAAE;AAAA,IAClC;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,wBAA8B;AAC5B,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,YAAY,SAAuB;AACjC,UAAM,QAAQ,KAAK,OAAO,IAAI,OAAO;AACrC,QAAI,OAAO;AACT,YAAM,QAAQ;AACd,WAAK,OAAO,OAAO,OAAO;AAC1B,WAAK,gBAAgB,OAAO,OAAO;AACnC,WAAK,aAAa,OAAO,OAAO;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,SAAS,SAA4C;AACnD,WAAO,KAAK,OAAO,IAAI,OAAO;AAAA,EAChC;AAAA,EAEA,KAAK,MAAe,QAAiB,UAAyB;AAC5D,QAAI,CAAC,KAAK,eAAe;AACvB,YAAM,IAAI,MAAM,qEAAqE;AAAA,IACvF;AAEA,UAAM,YAAY,QAAQ,IAAI;AAC9B,UAAM,YAAYA,cAAa;AAE/B,SAAK,qBAAqB;AAE1B,UAAM,kBAAkB,UAAU;AAClC,SAAK,OAAO,QAAQ,CAAC,UAAU;AAC7B,YAAM,YAAY;AAClB,YAAM,aAAa,WAAW,eAAe;AAAA,IAC/C,CAAC;AAGD,QAAI,aAAa,QAAW;AAC1B,WAAK,qBAAqB,UAAU,aAAa,MAAM;AACrD,aAAK,qBAAqB;AAC1B,YAAI;AACF,eAAK,6BAA6B;AAAA,QACpC,SAAS,KAAK;AACZ,kBAAQ,KAAK,8DAA8D,GAAG;AAAA,QAChF;AAAA,MACF,GAAG,kBAAkB,QAAQ;AAAA,IAC/B;AAGA,QAAI;AAGF,UAAI,UAAU,UAAU,WAAW;AACjC,kBAAU,KAAK;AAAA,MACjB;AACA,WAAK,OAAO,QAAQ,CAAC,UAAU,MAAM,eAAe,CAAC;AAIrD,gBAAU,YAAY,KAAK;AAC3B,gBAAU,UAAU,KAAK;AACzB,gBAAU,OAAO,KAAK;AAMtB,WAAK,OAAO,QAAQ,CAAC,UAAU,MAAM,uBAAuB,eAAe,CAAC;AAE5E,UAAI,WAAW,QAAW;AACxB,kBAAU,MAAM,WAAW,MAAM;AAAA,MACnC,OAAO;AACL,kBAAU,MAAM,SAAS;AAAA,MAC3B;AAeA,MAAC,UAAkB,OAAO,cAAc;AAKxC,WAAK,OAAO,QAAQ,CAAC,UAAU;AAC7B,cAAM,oBAAoB,iBAAiB,SAAS;AAAA,MACtD,CAAC;AAAA,IACH,SAAS,KAAK;AAEZ,WAAK,qBAAqB;AAC1B,WAAK,OAAO,QAAQ,CAAC,UAAU,MAAM,YAAY,CAAC;AAClD,cAAQ;AAAA,QACN;AAAA,QACA;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,QAAc;AACZ,UAAM,YAAYA,cAAa;AAC/B,QAAI;AACF,gBAAU,MAAM;AAAA,IAClB,SAAS,KAAK;AACZ,cAAQ,KAAK,iDAAiD,GAAG;AAAA,IACnE;AAGA,SAAK,OAAO,QAAQ,CAAC,UAAU,MAAM,eAAe,CAAC;AACrD,SAAK,OAAO,QAAQ,CAAC,UAAU,MAAM,YAAY,CAAC;AAClD,SAAK,qBAAqB;AAAA,EAC5B;AAAA,EAEA,OAAa;AACX,UAAM,YAAYA,cAAa;AAC/B,QAAI;AACF,gBAAU,KAAK;AAAA,IACjB,SAAS,KAAK;AACZ,cAAQ,KAAK,gDAAgD,GAAG;AAAA,IAClE;AAKA,QAAI,KAAK,cAAc;AACrB,UAAI;AACF,kBAAU,IAAI,QAAQ,KAAK,YAAY;AAAA,MACzC,SAAS,KAAK;AACZ,gBAAQ,KAAK,oDAAoD,GAAG;AAAA,MACtE;AACA,WAAK,eAAe;AAAA,IACtB;AAEA,SAAK,OAAO,QAAQ,CAAC,UAAU;AAC7B,UAAI;AACF,cAAM,eAAe;AAAA,MACvB,SAAS,KAAK;AACZ,gBAAQ,KAAK,yDAAyD,MAAM,EAAE,MAAM,GAAG;AAAA,MACzF;AAAA,IACF,CAAC;AACD,SAAK,OAAO,QAAQ,CAAC,UAAU;AAC7B,UAAI;AACF,cAAM,YAAY;AAAA,MACpB,SAAS,KAAK;AACZ,gBAAQ,KAAK,wDAAwD,MAAM,EAAE,MAAM,GAAG;AAAA,MACxF;AAAA,IACF,CAAC;AACD,SAAK,qBAAqB;AAAA,EAC5B;AAAA,EAEA,cAAc,MAAoB;AAChC,SAAK,aAAa,OAAO,QAAQ,KAAK,SAAS,IAAI;AAAA,EACrD;AAAA,EAEA,QAAQ,SAAiB,QAAuB;AAC9C,UAAM,QAAQ,KAAK,OAAO,IAAI,OAAO;AACrC,QAAI,OAAO;AACT,YAAM,QAAQ,MAAM;AACpB,UAAI,QAAQ;AACV,aAAK,aAAa,IAAI,OAAO;AAAA,MAC/B,OAAO;AACL,aAAK,aAAa,OAAO,OAAO;AAAA,MAClC;AACA,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,mBAAyB;AAC/B,UAAM,kBAAkB,KAAK,aAAa,OAAO;AAEjD,SAAK,OAAO,QAAQ,CAAC,OAAO,OAAO;AACjC,UAAI,iBAAiB;AACnB,YAAI,CAAC,KAAK,aAAa,IAAI,EAAE,GAAG;AAC9B,gBAAM,QAAQ,IAAI;AAAA,QACpB,OAAO;AACL,gBAAM,gBAAgB,KAAK,gBAAgB,IAAI,EAAE,KAAK;AACtD,gBAAM,QAAQ,aAAa;AAAA,QAC7B;AAAA,MACF,OAAO;AACL,cAAM,gBAAgB,KAAK,gBAAgB,IAAI,EAAE,KAAK;AACtD,cAAM,QAAQ,aAAa;AAAA,MAC7B;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,QAAQ,SAAiB,OAAsB;AAC7C,UAAM,QAAQ,KAAK,OAAO,IAAI,OAAO;AACrC,QAAI,OAAO;AACT,WAAK,gBAAgB,IAAI,SAAS,KAAK;AACvC,YAAM,QAAQ,KAAK;AAAA,IACrB;AAAA,EACF;AAAA,EAEA,QAAQ,SAAkB,WAAmB,SAAuB;AAIlE,SAAK,eAAe;AACpB,SAAK,aAAa;AAClB,SAAK,WAAW;AAEhB,UAAM,YAAYA,cAAa;AAC/B,QAAI;AAKF,gBAAU,YAAY;AACtB,gBAAU,UAAU;AACpB,gBAAU,OAAO;AAAA,IACnB,SAAS,KAAK;AACZ,cAAQ,KAAK,yDAAyD,GAAG;AACzE;AAAA,IACF;AAEA,QAAI,WAAW,CAAC,KAAK,cAAc;AACjC,WAAK,eAAe,MAAM;AASxB,cAAM,cAAc,IAAI;AACxB,aAAK,OAAO,QAAQ,CAAC,UAAU;AAC7B,cAAI;AACF,kBAAM,eAAe;AACrB,kBAAM,YAAY;AAClB,kBAAM,uBAAuB,KAAK,UAAU;AAC5C,kBAAM,oBAAoB,KAAK,YAAY,WAAW;AACtD,kBAAM,aAAa,aAAa,KAAK,UAAU;AAAA,UACjD,SAAS,KAAK;AACZ,oBAAQ;AAAA,cACN,kDAAkD,MAAM,EAAE;AAAA,cAC1D;AAAA,YACF;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH;AACA,gBAAU,GAAG,QAAQ,KAAK,YAAY;AAAA,IACxC,WAAW,CAAC,WAAW,KAAK,cAAc;AACxC,gBAAU,IAAI,QAAQ,KAAK,YAAY;AACvC,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,iBAAyB;AACvB,WAAOA,cAAa,EAAE;AAAA,EACxB;AAAA,EAEA,OAAO,MAAoB;AACzB,IAAAA,cAAa,EAAE,UAAU;AAAA,EAC3B;AAAA,EAEA,UAAgB;AAKd,QAAI;AACF,WAAK,KAAK;AAAA,IACZ,SAAS,KAAK;AACZ,cAAQ,KAAK,gEAAgE,GAAG;AAAA,IAClF;AAEA,SAAK,OAAO,QAAQ,CAAC,UAAU;AAC7B,UAAI;AACF,cAAM,QAAQ;AAAA,MAChB,SAAS,KAAK;AACZ,gBAAQ,KAAK,8CAA8C,MAAM,EAAE,MAAM,GAAG;AAAA,MAC9E;AAAA,IACF,CAAC;AACD,SAAK,OAAO,MAAM;AAElB,QAAI,KAAK,gBAAgB;AACvB,UAAI;AACF,aAAK,eAAe;AAAA,MACtB,SAAS,KAAK;AACZ,gBAAQ,KAAK,4DAA4D,GAAG;AAAA,MAC9E;AAAA,IACF;AAEA,QAAI;AACF,WAAK,aAAa,QAAQ;AAAA,IAC5B,SAAS,KAAK;AACZ,cAAQ,KAAK,sDAAsD,GAAG;AAAA,IACxE;AAAA,EACF;AAAA,EAEA,IAAI,UAAuB;AACzB,WAAOC,YAAW;AAAA,EACpB;AAAA,EAEA,IAAI,aAAqB;AACvB,WAAOA,YAAW,EAAE;AAAA,EACtB;AAAA,EAEA,sBAAsB,UAA4B;AAChD,SAAK,6BAA6B;AAAA,EACpC;AACF;;;AKxbA,SAAS,YAAY,qBAAqB;AAmCnC,SAAS,mBAAmB,IAAoB;AACrD,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI;AAC9B;AAGA,IAAM,sBAAsB;AAKrB,SAAS,kBACd,YACA,MACoB;AACpB,SAAO,WAAW,IAAI,GAAG;AAC3B;AAMO,SAAS,eAAe,SAAmC;AAChE,QAAM,SAAS,IAAI,aAAa,QAAQ,MAAM;AAC9C,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,WAAO,CAAC,IAAI,QAAQ,CAAC,IAAI;AAAA,EAC3B;AACA,SAAO;AACT;AAiCO,SAAS,sBAAsB,QAAoC;AACxE,QAAM,EAAE,UAAU,iBAAiB,eAAe,YAAY,UAAU,gBAAgB,IACtF;AAGF,QAAM,UACJ,oBAAoB,SAAY,kBAAkB,kBAAkB,MAAM,gBAAgB;AAG5F,QAAM,iBAAiB,WAAW,UAAU,cAAc,WAAW,mBAAmB;AAExF,SAAO,KAAK,IAAI,GAAG,iBAAiB,EAAE;AACxC;AAyBO,SAAS,uBACd,QACkD;AAClD,QAAM,EAAE,YAAY,OAAO,IAAI;AAG/B,QAAM,WAAW,kBAAkB,YAAY,cAAc,WAAW,KAAK;AAG7E,QAAM,eACJ,OAAO,aACN,kBAAkB,YAAY,cAAc,oBAAoB,KAAK,MACrE,kBAAkB,YAAY,cAAc,0BAA0B,KAAK,KAAK;AACnF,QAAM,aACJ,OAAO,WACN,kBAAkB,YAAY,cAAc,kBAAkB,KAAK,MACnE,kBAAkB,YAAY,cAAc,wBAAwB,KAAK,KAAK;AAKjF,QAAM,YAAY,eAAe,OAAO;AACxC,QAAM,UAAU,aAAa,OAAO;AAGpC,QAAM,eAAe;AAAA,IACnB,kBAAkB,YAAY,cAAc,YAAY,KAAK;AAAA,EAC/D;AACA,QAAM,aAAa;AAAA,IACjB,kBAAkB,YAAY,cAAc,UAAU,KAAK;AAAA,EAC7D;AACA,QAAM,cAAc;AAAA,IAClB,kBAAkB,YAAY,cAAc,WAAW,KAAK;AAAA,EAC9D;AACA,QAAM,gBAAgB,KAAK;AAAA,IACzB,mBAAmB,kBAAkB,YAAY,cAAc,aAAa,KAAK,KAAM;AAAA,IACvF;AAAA,EACF;AAIA,QAAM,YAAY,kBAAkB,YAAY,cAAc,aAAa,KAAK;AAChF,QAAM,gBAAgB,KAAK,IAAI,IAAI,CAAC,YAAY,GAAG;AAEnD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAYO,IAAM,iBAAN,MAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAU1B,YAAY,SAA4B;AATxC,SAAQ,MAAyB;AACjC,SAAQ,mBAA6C,oBAAI,IAAI;AAW3D,SAAK,UAAU,WAAW,IAAI,oBAAoB,GAAG,GAAG,KAAK;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAK,KAAa,QAAqC;AAC3D,UAAM,WAAW,MAAM,MAAM,KAAK,EAAE,OAAO,CAAC;AAC5C,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,6BAA6B,GAAG,KAAK,SAAS,UAAU,EAAE;AAAA,IAC5E;AACA,UAAM,cAAc,MAAM,SAAS,YAAY;AAC/C,QAAI;AACF,WAAK,MAAM,IAAI,WAAW,IAAI,WAAW,WAAW,CAAC;AAAA,IACvD,SAAS,KAAK;AACZ,YAAM,IAAI;AAAA,QACR,6BAA6B,GAAG,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACvF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,MAAyB;AACtC,QAAI;AACF,WAAK,MAAM,IAAI,WAAW,IAAI,WAAW,IAAI,CAAC;AAAA,IAChD,SAAS,KAAK;AACZ,YAAM,IAAI;AAAA,QACR,0CAA0C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MAC5F;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,WAAoB;AACtB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,eACE,UACA,aAAqB,GACrB,eAAuB,GACC;AACxB,QAAI,CAAC,KAAK,IAAK,QAAO;AAEtB,UAAM,UAAU,KAAK,IAAI,WAAW,UAAU,YAAY,YAAY;AACtE,QAAI,CAAC,QAAS,QAAO;AAErB,UAAM,SAAS,QAAQ;AACvB,UAAM,cAAc,KAAK,IAAI,QAAQ,QAAQ,MAAM;AAGnD,QAAI,SAAS,KAAK,iBAAiB,IAAI,WAAW;AAClD,QAAI,CAAC,QAAQ;AACX,eAAS,KAAK,mBAAmB,OAAO,MAAM,OAAO,OAAO,UAAU;AACtE,WAAK,iBAAiB,IAAI,aAAa,MAAM;AAAA,IAC/C;AAIA,UAAM,eAAe,sBAAsB;AAAA,MACzC;AAAA,MACA,iBAAiB,kBAAkB,QAAQ,YAAY,cAAc,iBAAiB;AAAA,MACtF,eAAe,OAAO,OAAO;AAAA,MAC7B,YAAY,kBAAkB,QAAQ,YAAY,cAAc,UAAU,KAAK;AAAA,MAC/E,UAAU,kBAAkB,QAAQ,YAAY,cAAc,QAAQ,KAAK;AAAA,MAC3E,iBAAiB,OAAO,OAAO,mBAAmB;AAAA,IACpD,CAAC;AAGD,UAAM,kBAAkB,uBAAuB;AAAA,MAC7C,YAAY,QAAQ;AAAA,MACpB,QAAQ,QAAQ,OAAO;AAAA,IACzB,CAAC;AAED,WAAO,EAAE,QAAQ,cAAc,GAAG,gBAAgB;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,mBAAmB,MAAkB,YAAiC;AAC5E,UAAM,SAAS,eAAe,IAAI;AAClC,UAAM,SAAS,KAAK,QAAQ,aAAa,GAAG,OAAO,QAAQ,UAAU;AACrE,WAAO,eAAe,CAAC,EAAE,IAAI,MAAM;AACnC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,SAAK,iBAAiB,MAAM;AAC5B,SAAK,MAAM;AAAA,EACb;AACF;;;ACjTA,SAAS,SAAS,kBAAkB;AAEpC,IAAI,oBAAoC;AAQjC,SAAS,mBAA4B;AAC1C,MAAI,CAAC,mBAAmB;AACtB,wBAAoB,IAAI,QAAQ;AAChC,eAAW,iBAAiB;AAAA,EAC9B;AACA,SAAO;AACT;AAOO,SAAS,wBAAsC;AACpD,SAAO,iBAAiB,EAAE;AAC5B;AAOO,SAAS,uBAAgC;AAC9C,SAAO,iBAAiB;AAC1B;AAOA,eAAsB,2BAA0C;AAC9D,QAAM,UAAU,iBAAiB;AACjC,MAAI,QAAQ,UAAU,WAAW;AAC/B,UAAM,QAAQ,OAAO;AAAA,EACvB;AACF;AAMO,SAAS,6BAAgD;AAC9D,SAAO,mBAAmB,WAAW,SAAS;AAChD;AAMA,eAAsB,0BAAyC;AAC7D,MAAI,qBAAqB,kBAAkB,WAAW,UAAU,UAAU;AACxE,UAAM,kBAAkB,MAAM;AAC9B,wBAAoB;AAAA,EACtB;AACF;;;AC1DA,SAAS,cAAAC,mBAAkB;AAG3B,IAAM,gBAAgB,oBAAI,IAA6C;AAGvE,IAAM,wBAAwB,oBAAI,IAA6B;AAiBxD,SAAS,qBAAqB,QAAiD;AAEpF,MAAI,cAAc,IAAI,MAAM,GAAG;AAC7B,WAAO,cAAc,IAAI,MAAM;AAAA,EACjC;AAGA,QAAM,UAAUA,YAAW;AAC3B,QAAM,SAAS,QAAQ,wBAAwB,MAAM;AACrD,gBAAc,IAAI,QAAQ,MAAM;AAGhC,QAAM,UAAU,MAAM;AACpB,WAAO,WAAW;AAClB,kBAAc,OAAO,MAAM;AAC3B,0BAAsB,OAAO,MAAM;AAGnC,WAAO,oBAAoB,SAAS,OAAO;AAC3C,WAAO,oBAAoB,YAAY,OAAO;AAAA,EAChD;AAEA,wBAAsB,IAAI,QAAQ,OAAO;AAGzC,SAAO,iBAAiB,SAAS,OAAO;AACxC,SAAO,iBAAiB,YAAY,OAAO;AAE3C,SAAO;AACT;AAUO,SAAS,yBAAyB,QAA2B;AAClE,QAAM,UAAU,sBAAsB,IAAI,MAAM;AAChD,MAAI,SAAS;AACX,YAAQ;AAAA,EACV;AACF;AAQO,SAAS,qBAAqB,QAA8B;AACjE,SAAO,cAAc,IAAI,MAAM;AACjC;;;AC7FA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAOP,SAAS,OAAAC,YAAW;AAQb,SAAS,kBAAkB,SAA8C;AAC9E,MAAI,UAA8B;AAClC,MAAI,aAAa;AACjB,MAAI,qBAAqB;AACzB,MAAI,eAAe;AACnB,MAAI,aAAa;AACjB,MAAI,WAAW;AACf,MAAI,oBAAoB;AAGxB,WAAS,kBAAkB,GAAgB,OAAwB;AACjE,UAAM,aAAa,MAAM,MAAM,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,SAAS;AAC1E,UAAM,YAAY,MAAM,MAAM,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,SAAS,CAAC;AAEjF,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,YAAY,KAAK,IAAI,GAAG,WAAW,IAAI,aAAa,CAAC;AAC3D,YAAM,UAAU,KAAK,IAAI,GAAG,WAAW,IAAI,WAAW,CAAC;AAEvD,YAAM,WAAkB;AAAA,QACtB,IAAI,MAAM;AAAA,QACV,MAAM,MAAM;AAAA,QACZ,MAAM,MAAM;AAAA,QACZ,OAAO,MAAM;AAAA,QACb,QAAQ,MAAM;AAAA,QACd,WAAW,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,MACF;AAEA,YAAM,YAAwB,WAAW,IAAI,CAAC,UAAU;AAAA,QACtD,QAAQ,KAAK;AAAA,QACb,WAAW,cAAc,IAAI,IAAI;AAAA,QACjC,UAAU,iBAAiB,IAAI;AAAA,QAC/B,QAAQ,eAAe,IAAI;AAAA,QAC3B,QAAQ,KAAK;AAAA,QACb,SAAS,KAAK;AAAA,QACd,MAAM,KAAK;AAAA,MACb,EAAE;AAEF,QAAE,SAAS;AAAA,QACT,OAAO;AAAA,QACP,OAAO;AAAA,QACP,SAAS,MAAM;AAAA,MACjB,CAAC;AAAA,IACH;AAEA,QAAI,UAAU,SAAS,GAAG;AACxB,YAAM,YAAY,KAAK,IAAI,GAAG,UAAU,IAAI,aAAa,CAAC;AAC1D,YAAM,UAAU,KAAK,IAAI,GAAG,UAAU,IAAI,WAAW,CAAC;AAEtD,YAAM,UAAU,WAAW,SAAS,IAAI,GAAG,MAAM,EAAE,UAAU,MAAM;AAEnE,YAAM,WAAkB;AAAA,QACtB,IAAI;AAAA,QACJ,MAAM,MAAM;AAAA,QACZ,MAAM,MAAM;AAAA,QACZ,OAAO,MAAM;AAAA,QACb,QAAQ,MAAM;AAAA,QACd,WAAW,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,MACF;AAEA,YAAM,gBAAgC,UAAU,IAAI,CAAC,UAAU;AAAA,QAC7D,OAAO,KAAK;AAAA,QACZ,WAAW,cAAc,IAAI,IAAI;AAAA,QACjC,UAAU,iBAAiB,IAAI;AAAA,QAC/B,QAAQ,eAAe,IAAI;AAAA,MAC7B,EAAE;AAEF,UAAI,SAAS,gBAAgB,UAAU;AACrC,cAAM,YAAY,UAAU,CAAC;AAC7B,cAAM,cAAc,UAAU;AAC9B,cAAM,eAAe,gBAAgB;AACrC,cAAM,gBAAgB,UAAU,eAAe;AAE/C,UAAE,kBAAkB;AAAA,UAClB,OAAO;AAAA,UACP,OAAO;AAAA,UACP,gBAAgB,QAAQ;AAAA,UACxB;AAAA,UACA;AAAA,UACA,SAAS,MAAM;AAAA,QACjB,CAAC;AAAA,MACH,OAAO;AACL,YAAI,SAAS,gBAAgB;AAC3B,kBAAQ;AAAA,YACN,uDAAuD,MAAM,IAAI;AAAA,UACnE;AAAA,QACF;AACA,UAAE,aAAa;AAAA,UACb,OAAO;AAAA,UACP,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,QACjB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,WAAS,aAAa,QAA2B;AAC/C,QAAI,SAAS;AACX,UAAI;AACF,gBAAQ,QAAQ;AAAA,MAClB,SAAS,KAAK;AACZ,gBAAQ,KAAK,wEAAwE,GAAG;AAAA,MAC1F;AACA,gBAAU;AAAA,IACZ;AAEA;AACA,UAAM,aAAa;AAEnB,cAAU,IAAI,YAAY;AAAA,MACxB,SAAS,SAAS;AAAA,IACpB,CAAC;AAKD,QAAI,mBAAmB;AACrB,cAAQ,KAAK,EAAE,MAAM,CAAC,QAAQ;AAC5B,gBAAQ;AAAA,UACN;AAAA,UAEA;AAAA,QACF;AACA,4BAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAEA,eAAW,SAAS,QAAQ;AAC1B,wBAAkB,SAAS,KAAK;AAAA,IAClC;AACA,YAAQ,sBAAsB;AAC9B,YAAQ,QAAQ,cAAc,YAAY,QAAQ;AAElD,YAAQ,sBAAsB,MAAM;AAClC,UAAI,eAAe,oBAAoB;AACrC,qBAAa;AAAA,MACf;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,MAAM,OAAsB;AAC1B,UAAI,SAAS;AACX,cAAM,QAAQ,KAAK;AACnB,4BAAoB;AAAA,MACtB;AAAA,IACF;AAAA,IAEA,UAAU,QAA2B;AACnC,mBAAa,MAAM;AAAA,IACrB;AAAA,IAEA,SAAS,OAAwB;AAC/B,UAAI,CAAC,SAAS;AACZ,cAAM,IAAI;AAAA,UACR;AAAA,QAEF;AAAA,MACF;AACA,wBAAkB,SAAS,KAAK;AAChC,cAAQ,sBAAsB;AAAA,IAChC;AAAA,IAEA,YAAY,SAAuB;AACjC,UAAI,CAAC,QAAS;AACd,cAAQ,YAAY,OAAO;AAC3B,cAAQ,sBAAsB;AAAA,IAChC;AAAA,IAEA,KAAK,WAAmB,SAAwB;AAC9C,UAAI,CAAC,SAAS;AACZ,gBAAQ;AAAA,UACN;AAAA,QAEF;AACA;AAAA,MACF;AACA,YAAM,WAAW,YAAY,SAAY,UAAU,YAAY;AAC/D,cAAQ,KAAKA,KAAI,GAAG,WAAW,QAAQ;AAGvC,mBAAa;AAAA,IACf;AAAA,IAEA,QAAc;AACZ,eAAS,MAAM;AACf,mBAAa;AAAA,IACf;AAAA,IAEA,OAAa;AACX,eAAS,KAAK;AACd,mBAAa;AAAA,IACf;AAAA,IAEA,KAAK,MAAoB;AACvB,eAAS,OAAO,IAAI;AAAA,IACtB;AAAA,IAEA,iBAAyB;AACvB,aAAO,SAAS,eAAe,KAAK;AAAA,IACtC;AAAA,IAEA,YAAqB;AACnB,aAAO;AAAA,IACT;AAAA,IAEA,gBAAgB,QAAsB;AACpC,eAAS,cAAc,MAAM;AAAA,IAC/B;AAAA,IAEA,eAAe,SAAiB,QAAsB;AACpD,eAAS,SAAS,OAAO,GAAG,UAAU,MAAM;AAAA,IAC9C;AAAA,IAEA,aAAa,SAAiB,OAAsB;AAClD,eAAS,QAAQ,SAAS,KAAK;AAAA,IACjC;AAAA,IAEA,aAAa,SAAiB,QAAuB;AACnD,eAAS,QAAQ,SAAS,MAAM;AAAA,IAClC;AAAA,IAEA,YAAY,SAAiB,KAAmB;AAC9C,eAAS,SAAS,OAAO,GAAG,OAAO,GAAG;AAAA,IACxC;AAAA,IAEA,QAAQ,SAAkBC,QAAe,KAAmB;AAC1D,qBAAe;AACf,mBAAaA;AACb,iBAAW;AACX,eAAS,QAAQ,SAASA,QAAO,GAAG;AAAA,IACtC;AAAA,IAEA,UAAgB;AACd,UAAI;AACF,iBAAS,QAAQ;AAAA,MACnB,SAAS,KAAK;AACZ,gBAAQ,KAAK,gDAAgD,GAAG;AAAA,MAClE;AACA,gBAAU;AACV,mBAAa;AAAA,IACf;AAAA,EACF;AACF;","names":["Volume","getDestination","getTransport","getContext","clipStartTime","Volume","Gain","Panner","getDestination","getContext","Volume","Panner","Gain","getDestination","now","getContext","Volume","Gain","Panner","Part","getDestination","getContext","Volume","Panner","Gain","getDestination","Part","getContext","Volume","getDestination","getTransport","getContext","getContext","now","start"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waveform-playlist/playout",
3
- "version": "10.1.2",
3
+ "version": "10.3.0",
4
4
  "description": "Playout engine for waveform-playlist using Tone.js",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -39,11 +39,11 @@
39
39
  "tsup": "^8.0.1",
40
40
  "typescript": "^5.3.3",
41
41
  "vitest": "^3.0.0",
42
- "@waveform-playlist/engine": "^10.1.2"
42
+ "@waveform-playlist/engine": "^10.3.0"
43
43
  },
44
44
  "dependencies": {
45
45
  "soundfont2": "^0.5.0",
46
- "@waveform-playlist/core": "10.1.2"
46
+ "@waveform-playlist/core": "10.3.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@waveform-playlist/engine": ">=7.0.0",