@waveform-playlist/browser 13.1.3 → 14.0.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.d.mts +34 -433
- package/dist/index.d.ts +34 -433
- package/dist/index.js +610 -2371
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +507 -2291
- package/dist/index.mjs.map +1 -1
- package/dist/tone.d.mts +429 -0
- package/dist/tone.d.ts +429 -0
- package/dist/tone.js +1963 -0
- package/dist/tone.js.map +1 -0
- package/dist/tone.mjs +1958 -0
- package/dist/tone.mjs.map +1 -0
- package/package.json +23 -8
package/dist/tone.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tone.ts","../src/hooks/useAudioTracks.ts","../src/hooks/useAudioEffects.ts","../src/hooks/useDynamicEffects.ts","../src/effects/effectDefinitions.ts","../src/effects/effectFactory.ts","../src/hooks/useTrackDynamicEffects.ts","../src/hooks/useExportWav.ts","../src/utils/wavEncoder.ts","../src/hooks/useDynamicTracks.ts","../src/hooks/useOutputMeter.ts","../src/components/ExportControls.tsx","../src/WaveformPlaylistContext.tsx"],"sourcesContent":["// Tone.js batteries-included surface for @waveform-playlist/browser.\n// Import from '@waveform-playlist/browser/tone'. Everything here depends on the\n// optional peers `tone` and `@waveform-playlist/playout` (#510 — keeps the core\n// `@waveform-playlist/browser` entry free of any static tone/playout import).\n\nexport { useAudioTracks } from './hooks/useAudioTracks';\nexport type { AudioTrackConfig } from './hooks/useAudioTracks';\n\nexport { useMasterAnalyser } from './hooks/useAudioEffects';\n\nexport { useDynamicEffects } from './hooks/useDynamicEffects';\nexport type { UseDynamicEffectsReturn, ActiveEffect } from './hooks/useDynamicEffects';\n\nexport { useTrackDynamicEffects } from './hooks/useTrackDynamicEffects';\nexport type {\n UseTrackDynamicEffectsReturn,\n TrackActiveEffect,\n TrackEffectsState,\n} from './hooks/useTrackDynamicEffects';\n\nexport { useExportWav } from './hooks/useExportWav';\nexport type { ExportOptions, ExportResult, UseExportWavReturn } from './hooks/useExportWav';\n\nexport { useDynamicTracks } from './hooks/useDynamicTracks';\nexport type { TrackSource, TrackLoadError, UseDynamicTracksReturn } from './hooks/useDynamicTracks';\n\nexport { useOutputMeter } from './hooks/useOutputMeter';\nexport type { UseOutputMeterOptions, UseOutputMeterReturn } from './hooks/useOutputMeter';\n\nexport {\n effectDefinitions,\n effectCategories,\n getEffectDefinition,\n getEffectsByCategory,\n createEffectInstance,\n createEffectChain,\n} from './effects';\nexport type { EffectDefinition, EffectParameter, ParameterType, EffectInstance } from './effects';\n\nexport { ExportWavButton } from './components/ExportControls';\nexport type { ExportWavButtonProps } from './components/ExportControls';\n","import { useState, useEffect, useRef, useMemo } from 'react';\nimport {\n ClipTrack,\n createTrack,\n createClipFromSeconds,\n type Fade,\n type TrackEffectsFunction,\n type WaveformDataObject,\n type RenderMode,\n type SpectrogramConfig,\n type ColorMapValue,\n} from '@waveform-playlist/core';\nimport * as Tone from 'tone';\n\n/**\n * Configuration for a single audio track to load\n *\n * Audio can be provided in three ways:\n * 1. `src` - URL to fetch and decode (standard loading)\n * 2. `audioBuffer` - Pre-loaded AudioBuffer (skip fetch/decode)\n * 3. `waveformData` only - Peaks-first rendering (audio loads later)\n *\n * For peaks-first rendering, just provide `waveformData` - the sample rate\n * and duration are derived from the waveform data automatically.\n */\nexport interface AudioTrackConfig {\n /** URL to audio file - used if audioBuffer not provided */\n src?: string;\n /** Pre-loaded AudioBuffer - skips fetch/decode if provided */\n audioBuffer?: AudioBuffer;\n name?: string;\n muted?: boolean;\n soloed?: boolean;\n volume?: number;\n pan?: number;\n color?: string;\n effects?: TrackEffectsFunction;\n // Multi-clip support\n startTime?: number; // When the clip starts on the timeline (default: 0)\n duration?: number; // Duration of the clip (default: full audio duration)\n offset?: number; // Offset into the source audio file (default: 0)\n // Fade support\n fadeIn?: Fade; // Fade in configuration\n fadeOut?: Fade; // Fade out configuration\n // Pre-computed waveform data (BBC audiowaveform format)\n // For peaks-first rendering, provide this without audioBuffer/src\n // Sample rate and duration are derived from waveformData.sample_rate and waveformData.duration\n waveformData?: WaveformDataObject;\n /** Visualization render mode: 'waveform' | 'spectrogram' | 'both'. Default: 'waveform' */\n renderMode?: RenderMode;\n /** Spectrogram configuration (FFT size, window, frequency scale, etc.) */\n spectrogramConfig?: SpectrogramConfig;\n /** Spectrogram color map name or custom color array */\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Options for useAudioTracks hook\n */\nexport interface UseAudioTracksOptions {\n /**\n * When true, all tracks render immediately as placeholders with clip geometry\n * from the config. Audio fills in progressively as files decode, and peaks\n * render as each buffer becomes available. Use with `deferEngineRebuild={loading}`\n * on the provider for a single engine build when all tracks are ready.\n *\n * Requires `duration` or `waveformData` in each config so clip dimensions are known upfront.\n * Default: false\n */\n immediate?: boolean;\n /** @deprecated Use `immediate` instead. */\n progressive?: boolean;\n}\n\n/** Build a ClipTrack from config + optional audioBuffer, preserving stable IDs. */\nfunction buildTrackFromConfig(\n config: AudioTrackConfig,\n index: number,\n audioBuffer: AudioBuffer | undefined,\n stableIds: Map<number, { trackId: string; clipId: string }>,\n contextSampleRate: number = 48000\n): ClipTrack | null {\n const buffer = audioBuffer ?? config.audioBuffer;\n\n // Determine if we have enough info to create the track\n // Prefer buffer/waveformData sample rate; fall back to the AudioContext's rate\n const sampleRate = buffer?.sampleRate ?? config.waveformData?.sample_rate ?? contextSampleRate;\n const sourceDuration =\n buffer?.duration ??\n config.waveformData?.duration ??\n (config.duration != null ? config.duration + (config.offset ?? 0) : undefined);\n\n if (sourceDuration === undefined) {\n console.warn(\n `[waveform-playlist] Track ${index + 1} (\"${config.name ?? 'unnamed'}\"): ` +\n `Cannot create track — provide duration, audioBuffer, or waveformData with duration.`\n );\n return null;\n }\n\n const clip = createClipFromSeconds({\n audioBuffer: buffer,\n sampleRate,\n sourceDuration,\n startTime: config.startTime ?? 0,\n duration: config.duration ?? sourceDuration,\n offset: config.offset ?? 0,\n name: config.name || `Track ${index + 1}`,\n fadeIn: config.fadeIn,\n fadeOut: config.fadeOut,\n waveformData: config.waveformData,\n });\n\n // Validate clip values\n if (isNaN(clip.startSample) || isNaN(clip.durationSamples) || isNaN(clip.offsetSamples)) {\n console.error(\n `[waveform-playlist] Invalid clip values for track ${index + 1} (\"${config.name ?? 'unnamed'}\"): ` +\n `startSample=${clip.startSample}, durationSamples=${clip.durationSamples}, offsetSamples=${clip.offsetSamples}`\n );\n return null;\n }\n\n const track: ClipTrack = {\n ...createTrack({\n name: config.name || `Track ${index + 1}`,\n clips: [clip],\n muted: config.muted ?? false,\n soloed: config.soloed ?? false,\n volume: config.volume ?? 1.0,\n pan: config.pan ?? 0,\n color: config.color,\n }),\n effects: config.effects,\n renderMode: config.renderMode,\n spectrogramConfig: config.spectrogramConfig,\n spectrogramColorMap: config.spectrogramColorMap,\n };\n\n // Preserve stable IDs across rebuilds so React doesn't unmount/remount tracks\n const existingIds = stableIds.get(index);\n if (existingIds) {\n track.id = existingIds.trackId;\n track.clips[0] = { ...track.clips[0], id: existingIds.clipId };\n } else {\n stableIds.set(index, { trackId: track.id, clipId: track.clips[0].id });\n }\n\n return track;\n}\n\n/**\n * Hook to load audio from URLs and convert to ClipTrack format\n *\n * This hook fetches audio files, decodes them, and creates ClipTrack objects\n * with a single clip per track. Supports custom positioning for multi-clip arrangements.\n *\n * @param configs - Array of audio track configurations\n * @param options - Optional configuration for loading behavior\n * @returns Object with tracks array, loading state, and progress info\n *\n * @example\n * ```typescript\n * // Basic usage (clips positioned at start)\n * const { tracks, loading, error } = useAudioTracks([\n * { src: 'audio/vocals.mp3', name: 'Vocals' },\n * { src: 'audio/drums.mp3', name: 'Drums' },\n * ]);\n *\n * // Immediate rendering with deferred engine build (recommended for multi-track)\n * const { tracks, loading } = useAudioTracks(\n * [\n * { src: 'audio/vocals.mp3', name: 'Vocals', duration: 30 },\n * { src: 'audio/drums.mp3', name: 'Drums', duration: 30 },\n * ],\n * { immediate: true }\n * );\n * // All tracks render instantly as placeholders, peaks fill in as files load\n * return (\n * <WaveformPlaylistProvider tracks={tracks} deferEngineRebuild={loading}>\n * ...\n * </WaveformPlaylistProvider>\n * );\n *\n * // Pre-loaded AudioBuffer (skip fetch/decode)\n * const { tracks } = useAudioTracks([\n * { audioBuffer: myPreloadedBuffer, name: 'Pre-loaded' },\n * ]);\n *\n * // Peaks-first rendering (instant visual, audio loads later)\n * const { tracks } = useAudioTracks([\n * { waveformData: preloadedPeaks, name: 'Peaks Only' }, // Renders immediately\n * ]);\n * ```\n */\nexport function useAudioTracks(configs: AudioTrackConfig[], options: UseAudioTracksOptions = {}) {\n const { immediate = false, progressive = false } = options;\n // progressive is a deprecated alias for immediate\n const isImmediate = immediate || progressive;\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [loadedCount, setLoadedCount] = useState(0);\n\n const totalCount = configs.length;\n\n // For immediate mode: store loaded AudioBuffers by config index\n const [loadedBuffers, setLoadedBuffers] = useState<Map<number, AudioBuffer>>(new Map());\n\n // Stable track/clip IDs across rebuilds (immediate mode)\n const stableIdsRef = useRef<Map<number, { trackId: string; clipId: string }>>(new Map());\n\n // AudioContext sample rate, populated once loadTracks runs.\n // Used as fallback when neither buffer nor waveformData provides a sample rate.\n const contextSampleRateRef = useRef<number>(48000);\n\n // For immediate mode: derive tracks from configs + loaded buffers.\n // Runs on mount (creates placeholders) and each time a buffer loads (attaches audioBuffer).\n const derivedTracks = useMemo(() => {\n if (!isImmediate) return null;\n\n const result: ClipTrack[] = [];\n for (let i = 0; i < configs.length; i++) {\n const track = buildTrackFromConfig(\n configs[i],\n i,\n loadedBuffers.get(i),\n stableIdsRef.current,\n contextSampleRateRef.current\n );\n if (track) result.push(track);\n }\n return result;\n }, [isImmediate, configs, loadedBuffers]);\n\n // Initialize tracks with derivedTracks so immediate-mode placeholders\n // appear on the very first render (no flash of empty content).\n const [tracks, setTracks] = useState<ClipTrack[]>(derivedTracks ?? []);\n\n // Sync derived tracks into state synchronously during render (not useEffect).\n // useEffect sync causes a 1-render lag — if deferEngineRebuild flips in a\n // separate batch, the provider sees stale tracks and rebuilds the engine twice.\n const prevDerivedRef = useRef(derivedTracks);\n if (derivedTracks !== prevDerivedRef.current) {\n prevDerivedRef.current = derivedTracks;\n if (derivedTracks) setTracks(derivedTracks);\n }\n\n useEffect(() => {\n if (configs.length === 0) {\n setTracks([]);\n setLoading(false);\n setLoadedCount(0);\n return;\n }\n\n let cancelled = false;\n const abortController = new AbortController();\n\n const loadTracks = async () => {\n try {\n setLoading(true);\n setError(null);\n setLoadedCount(0);\n\n if (isImmediate) {\n setLoadedBuffers(new Map());\n }\n\n const audioContext = Tone.getContext().rawContext as AudioContext;\n contextSampleRateRef.current = audioContext.sampleRate;\n\n // Process each config\n const loadPromises = configs.map(async (config, index) => {\n // Case 1: Already have audioBuffer - no loading needed\n if (config.audioBuffer) {\n if (isImmediate && !cancelled) {\n setLoadedBuffers((prev) => {\n const next = new Map(prev);\n next.set(index, config.audioBuffer!);\n return next;\n });\n setLoadedCount((prev) => prev + 1);\n return;\n }\n\n return buildTrackFromConfig(\n config,\n index,\n config.audioBuffer,\n stableIdsRef.current,\n audioContext.sampleRate\n );\n }\n\n // Case 2: Have waveformData but no src - peaks-only (no audio to load)\n if (!config.src && config.waveformData) {\n if (isImmediate && !cancelled) {\n // No buffer to load — track is already created from waveformData\n setLoadedCount((prev) => prev + 1);\n return;\n }\n\n return buildTrackFromConfig(\n config,\n index,\n undefined,\n stableIdsRef.current,\n audioContext.sampleRate\n );\n }\n\n // Case 3: Need to fetch and decode audio from src\n if (!config.src) {\n throw new Error(`Track ${index + 1}: Must provide src, audioBuffer, or waveformData`);\n }\n\n const response = await fetch(config.src, { signal: abortController.signal });\n if (!response.ok) {\n throw new Error(`Failed to fetch ${config.src}: ${response.statusText}`);\n }\n\n const arrayBuffer = await response.arrayBuffer();\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n\n // Validate audioBuffer\n if (!audioBuffer || !audioBuffer.sampleRate || !audioBuffer.duration) {\n throw new Error(`Invalid audio buffer for ${config.src}`);\n }\n\n if (isImmediate && !cancelled) {\n // Store buffer — useMemo will derive the updated track\n setLoadedBuffers((prev) => {\n const next = new Map(prev);\n next.set(index, audioBuffer);\n return next;\n });\n setLoadedCount((prev) => prev + 1);\n return;\n }\n\n return buildTrackFromConfig(\n config,\n index,\n audioBuffer,\n stableIdsRef.current,\n audioContext.sampleRate\n );\n });\n\n const loadedTracks = await Promise.all(loadPromises);\n\n if (!cancelled) {\n // For non-immediate mode: set all tracks at once\n if (!isImmediate) {\n const validTracks = loadedTracks.filter((t): t is ClipTrack => t != null);\n setTracks(validTracks);\n setLoadedCount(validTracks.length);\n }\n setLoading(false);\n }\n } catch (err) {\n if (!cancelled) {\n const errorMessage = err instanceof Error ? err.message : 'Unknown error loading audio';\n setError(errorMessage);\n setLoading(false);\n console.error(`[waveform-playlist] Error loading audio tracks: ${errorMessage}`);\n }\n }\n };\n\n loadTracks();\n\n // Cleanup: prevent state updates and abort in-flight fetches on unmount\n return () => {\n cancelled = true;\n abortController.abort();\n };\n }, [configs, isImmediate]);\n\n return { tracks, loading, error, loadedCount, totalCount };\n}\n","import { useRef, useCallback } from 'react';\nimport type { EffectsFunction } from '@waveform-playlist/playout';\n// Import Tone.js classes directly for tree-shaking\nimport { Analyser } from 'tone';\n\n/**\n * Hook for master effects with frequency analyzer\n * Returns the analyser ref and the effects function to pass to WaveformPlaylistProvider\n *\n * For more advanced effects (reverb, delay, filters, etc.), use useDynamicEffects instead.\n */\nexport const useMasterAnalyser = (fftSize: number = 256) => {\n const analyserRef = useRef<Analyser | null>(null);\n\n const masterEffects: EffectsFunction = useCallback(\n (masterGainNode, destination, _isOffline) => {\n // Create analyser and connect it in parallel to monitor the output\n const analyserNode = new Analyser('fft', fftSize);\n masterGainNode.connect(analyserNode);\n\n // Connect master to destination as normal\n masterGainNode.connect(destination);\n\n // Store analyser for visualization\n analyserRef.current = analyserNode;\n\n return function cleanup() {\n // Cleanup when playlist is destroyed\n analyserNode.dispose();\n analyserRef.current = null;\n };\n },\n [fftSize]\n );\n\n return { analyserRef, masterEffects };\n};\n","import { useState, useCallback, useRef, useEffect } from 'react';\nimport type { EffectsFunction } from '@waveform-playlist/playout';\nimport {\n effectDefinitions,\n getEffectDefinition,\n type EffectDefinition,\n} from '../effects/effectDefinitions';\nimport { createEffectInstance, type EffectInstance } from '../effects/effectFactory';\nimport { Analyser, Volume, ToneAudioNode } from 'tone';\n\nexport interface ActiveEffect {\n instanceId: string;\n effectId: string;\n definition: EffectDefinition;\n params: Record<string, number | string | boolean>;\n bypassed: boolean;\n}\n\nexport interface UseDynamicEffectsReturn {\n // State\n activeEffects: ActiveEffect[];\n availableEffects: EffectDefinition[];\n\n // Actions\n addEffect: (effectId: string) => void;\n removeEffect: (instanceId: string) => void;\n updateParameter: (\n instanceId: string,\n paramName: string,\n value: number | string | boolean\n ) => void;\n toggleBypass: (instanceId: string) => void;\n reorderEffects: (fromIndex: number, toIndex: number) => void;\n clearAllEffects: () => void;\n\n // For connecting to audio graph\n masterEffects: EffectsFunction;\n\n /**\n * Creates a fresh effects function for offline rendering.\n * This creates new effect instances that work in the offline AudioContext.\n */\n createOfflineEffectsFunction: () => EffectsFunction | undefined;\n\n // Analyser for visualization\n analyserRef: React.RefObject<Analyser | null>;\n}\n\n/**\n * Hook for managing a dynamic chain of audio effects with real-time parameter updates\n */\nexport function useDynamicEffects(fftSize: number = 256): UseDynamicEffectsReturn {\n // Track active effects in state (for UI)\n const [activeEffects, setActiveEffects] = useState<ActiveEffect[]>([]);\n\n // Ref to store current activeEffects for reading in callbacks (avoids stale closures)\n const activeEffectsRef = useRef<ActiveEffect[]>(activeEffects);\n activeEffectsRef.current = activeEffects;\n\n // Track effect instances (for audio processing)\n const effectInstancesRef = useRef<Map<string, EffectInstance>>(new Map());\n\n // Analyser for visualization\n const analyserRef = useRef<Analyser | null>(null);\n\n // Reference to the current audio graph nodes\n const graphNodesRef = useRef<{\n masterGainNode: Volume;\n destination: ToneAudioNode;\n analyserNode: Analyser;\n } | null>(null);\n\n // Rebuild the effect chain when effects change\n // Note: effects is passed as parameter to avoid stale closure issues\n const rebuildChain = useCallback((effects: ActiveEffect[]) => {\n const nodes = graphNodesRef.current;\n if (!nodes) return;\n\n const { masterGainNode, destination, analyserNode } = nodes;\n\n // Disconnect everything first\n try {\n masterGainNode.disconnect();\n } catch (e) {\n console.warn('[waveform-playlist] Error disconnecting master effects chain:', e);\n }\n\n // Get effect instances in order\n const instances = effects\n .map((ae) => effectInstancesRef.current.get(ae.instanceId))\n .filter((inst): inst is EffectInstance => inst !== undefined);\n\n if (instances.length === 0) {\n // No effects - connect directly to analyser -> destination\n masterGainNode.connect(analyserNode);\n analyserNode.connect(destination);\n } else {\n // Connect: masterGain -> effect1 -> effect2 -> ... -> analyser -> destination\n let currentNode: ToneAudioNode = masterGainNode;\n\n instances.forEach((inst) => {\n try {\n inst.disconnect();\n } catch (e) {\n console.warn(`[waveform-playlist] Error disconnecting effect \"${inst.id}\":`, e);\n }\n currentNode.connect(inst.effect);\n currentNode = inst.effect;\n });\n\n // Connect last effect to analyser\n currentNode.connect(analyserNode);\n analyserNode.connect(destination);\n }\n }, []);\n\n // Add a new effect\n const addEffect = useCallback((effectId: string) => {\n const definition = getEffectDefinition(effectId);\n if (!definition) {\n console.error(`Unknown effect: ${effectId}`);\n return;\n }\n\n // Build default params\n const params: Record<string, number | string | boolean> = {};\n definition.parameters.forEach((p) => {\n params[p.name] = p.default;\n });\n\n // Create the effect instance\n const instance = createEffectInstance(definition, params);\n effectInstancesRef.current.set(instance.instanceId, instance);\n\n // Add to state\n const newActiveEffect: ActiveEffect = {\n instanceId: instance.instanceId,\n effectId: definition.id,\n definition,\n params,\n bypassed: false,\n };\n\n setActiveEffects((prev) => [...prev, newActiveEffect]);\n }, []);\n\n // Remove an effect\n const removeEffect = useCallback((instanceId: string) => {\n const instance = effectInstancesRef.current.get(instanceId);\n if (instance) {\n instance.dispose();\n effectInstancesRef.current.delete(instanceId);\n }\n\n setActiveEffects((prev) => prev.filter((e) => e.instanceId !== instanceId));\n }, []);\n\n // Update a parameter in real-time\n const updateParameter = useCallback(\n (instanceId: string, paramName: string, value: number | string | boolean) => {\n // Update the actual effect instance\n const instance = effectInstancesRef.current.get(instanceId);\n if (instance) {\n instance.setParameter(paramName, value);\n }\n\n // Update state for UI\n setActiveEffects((prev) =>\n prev.map((e) =>\n e.instanceId === instanceId ? { ...e, params: { ...e.params, [paramName]: value } } : e\n )\n );\n },\n []\n );\n\n // Toggle bypass for an effect (uses wet parameter - 0 = bypass, restore original for active)\n const toggleBypass = useCallback((instanceId: string) => {\n // Get current state from ref to determine new bypassed value (avoids stale closure)\n const effect = activeEffectsRef.current.find((e) => e.instanceId === instanceId);\n if (!effect) return;\n\n const newBypassed = !effect.bypassed;\n\n // Update the actual effect instance\n // When bypassing: set wet to 0\n // When un-bypassing: restore the original wet value from params\n const instance = effectInstancesRef.current.get(instanceId);\n if (instance) {\n const originalWet = (effect.params.wet as number) ?? 1;\n instance.setParameter('wet', newBypassed ? 0 : originalWet);\n }\n\n // Update state for UI\n setActiveEffects((prev) =>\n prev.map((e) => (e.instanceId === instanceId ? { ...e, bypassed: newBypassed } : e))\n );\n }, []);\n\n // Reorder effects in the chain\n const reorderEffects = useCallback((fromIndex: number, toIndex: number) => {\n setActiveEffects((prev) => {\n const newEffects = [...prev];\n const [removed] = newEffects.splice(fromIndex, 1);\n newEffects.splice(toIndex, 0, removed);\n return newEffects;\n });\n }, []);\n\n // Clear all effects\n const clearAllEffects = useCallback(() => {\n // Dispose all instances\n effectInstancesRef.current.forEach((inst) => inst.dispose());\n effectInstancesRef.current.clear();\n\n setActiveEffects([]);\n }, []);\n\n // Rebuild chain when effects change\n useEffect(() => {\n rebuildChain(activeEffects);\n }, [activeEffects, rebuildChain]);\n\n // The effects function that gets passed to WaveformPlaylistProvider\n // This function is stable - it reads from refs at call time to avoid stale closures\n const masterEffects: EffectsFunction = useCallback(\n (masterGainNode, destination, _isOffline) => {\n // Create analyser for visualization\n const analyserNode = new Analyser('fft', fftSize);\n analyserRef.current = analyserNode;\n\n // Store references for rebuilding chain\n graphNodesRef.current = {\n masterGainNode,\n destination,\n analyserNode,\n };\n\n // Build initial chain - read from ref to get current state\n const effects = activeEffectsRef.current;\n const instances = effects\n .map((ae) => effectInstancesRef.current.get(ae.instanceId))\n .filter((inst): inst is EffectInstance => inst !== undefined);\n\n if (instances.length === 0) {\n // No effects - connect directly to analyser -> destination\n masterGainNode.connect(analyserNode);\n analyserNode.connect(destination);\n } else {\n // Connect: masterGain -> effect1 -> effect2 -> ... -> analyser -> destination\n let currentNode: ToneAudioNode = masterGainNode;\n\n instances.forEach((inst) => {\n currentNode.connect(inst.effect);\n currentNode = inst.effect;\n });\n\n // Connect last effect to analyser\n currentNode.connect(analyserNode);\n analyserNode.connect(destination);\n }\n\n return function cleanup() {\n analyserNode.dispose();\n analyserRef.current = null;\n graphNodesRef.current = null;\n };\n },\n [fftSize] // Only fftSize - reads effects from ref\n );\n\n // Cleanup on unmount\n useEffect(() => {\n const effectInstances = effectInstancesRef.current;\n return () => {\n effectInstances.forEach((inst) => inst.dispose());\n effectInstances.clear();\n };\n }, []);\n\n /**\n * Creates a fresh effects function for offline rendering.\n * This creates new effect instances in the offline context, avoiding the\n * AudioContext mismatch issue that occurs when reusing real-time effects.\n */\n const createOfflineEffectsFunction = useCallback((): EffectsFunction | undefined => {\n // Get non-bypassed effects\n const nonBypassedEffects = activeEffects.filter((e) => !e.bypassed);\n\n if (nonBypassedEffects.length === 0) {\n return undefined;\n }\n\n // Return a function that creates fresh effect instances\n return (masterGainNode: Volume, destination: ToneAudioNode, _isOffline: boolean) => {\n // Create fresh effect instances for offline context\n const offlineInstances: EffectInstance[] = [];\n\n for (const activeEffect of nonBypassedEffects) {\n const instance = createEffectInstance(activeEffect.definition, activeEffect.params);\n offlineInstances.push(instance);\n }\n\n if (offlineInstances.length === 0) {\n // No effects - connect directly\n masterGainNode.connect(destination);\n } else {\n // Connect: masterGain -> effect1 -> effect2 -> ... -> destination\n let currentNode: ToneAudioNode = masterGainNode;\n\n offlineInstances.forEach((inst) => {\n currentNode.connect(inst.effect);\n currentNode = inst.effect;\n });\n\n // Connect last effect to destination\n currentNode.connect(destination);\n }\n\n return function cleanup() {\n offlineInstances.forEach((inst) => inst.dispose());\n };\n };\n }, [activeEffects]);\n\n return {\n activeEffects,\n availableEffects: effectDefinitions,\n addEffect,\n removeEffect,\n updateParameter,\n toggleBypass,\n reorderEffects,\n clearAllEffects,\n masterEffects,\n createOfflineEffectsFunction,\n analyserRef,\n };\n}\n","/**\n * Effect definitions for all available Tone.js effects\n * Each effect has parameters with min/max/default values for UI controls\n */\n\nexport type ParameterType = 'number' | 'select' | 'boolean';\n\nexport interface EffectParameter {\n name: string;\n label: string;\n type: ParameterType;\n min?: number;\n max?: number;\n step?: number;\n default: number | string | boolean;\n unit?: string;\n options?: { value: string | number; label: string }[];\n}\n\nexport interface EffectDefinition {\n id: string;\n name: string;\n category: 'delay' | 'reverb' | 'modulation' | 'distortion' | 'filter' | 'dynamics' | 'spatial';\n description: string;\n parameters: EffectParameter[];\n}\n\nexport const effectDefinitions: EffectDefinition[] = [\n // === REVERB EFFECTS ===\n {\n id: 'reverb',\n name: 'Reverb',\n category: 'reverb',\n description: 'Simple convolution reverb with adjustable decay time',\n parameters: [\n {\n name: 'decay',\n label: 'Decay',\n type: 'number',\n min: 0.1,\n max: 10,\n step: 0.1,\n default: 1.5,\n unit: 's',\n },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n ],\n },\n {\n id: 'freeverb',\n name: 'Freeverb',\n category: 'reverb',\n description: 'Classic Schroeder/Moorer reverb with room size and dampening',\n parameters: [\n {\n name: 'roomSize',\n label: 'Room Size',\n type: 'number',\n min: 0,\n max: 1,\n step: 0.01,\n default: 0.7,\n },\n {\n name: 'dampening',\n label: 'Dampening',\n type: 'number',\n min: 0,\n max: 10000,\n step: 100,\n default: 3000,\n unit: 'Hz',\n },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n ],\n },\n {\n id: 'jcReverb',\n name: 'JC Reverb',\n category: 'reverb',\n description: 'Attempt at Roland JC-120 chorus reverb emulation',\n parameters: [\n {\n name: 'roomSize',\n label: 'Room Size',\n type: 'number',\n min: 0,\n max: 1,\n step: 0.01,\n default: 0.5,\n },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n ],\n },\n\n // === DELAY EFFECTS ===\n {\n id: 'feedbackDelay',\n name: 'Feedback Delay',\n category: 'delay',\n description: 'Delay line with feedback for echo effects',\n parameters: [\n {\n name: 'delayTime',\n label: 'Delay Time',\n type: 'number',\n min: 0,\n max: 1,\n step: 0.01,\n default: 0.25,\n unit: 's',\n },\n {\n name: 'feedback',\n label: 'Feedback',\n type: 'number',\n min: 0,\n max: 0.95,\n step: 0.01,\n default: 0.5,\n },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n ],\n },\n {\n id: 'pingPongDelay',\n name: 'Ping Pong Delay',\n category: 'delay',\n description: 'Stereo delay bouncing between left and right channels',\n parameters: [\n {\n name: 'delayTime',\n label: 'Delay Time',\n type: 'number',\n min: 0,\n max: 1,\n step: 0.01,\n default: 0.25,\n unit: 's',\n },\n {\n name: 'feedback',\n label: 'Feedback',\n type: 'number',\n min: 0,\n max: 0.95,\n step: 0.01,\n default: 0.5,\n },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n ],\n },\n\n // === MODULATION EFFECTS ===\n {\n id: 'chorus',\n name: 'Chorus',\n category: 'modulation',\n description: 'Creates thickness by layering detuned copies of the signal',\n parameters: [\n {\n name: 'frequency',\n label: 'Rate',\n type: 'number',\n min: 0.1,\n max: 10,\n step: 0.1,\n default: 1.5,\n unit: 'Hz',\n },\n {\n name: 'delayTime',\n label: 'Delay',\n type: 'number',\n min: 0,\n max: 20,\n step: 0.5,\n default: 3.5,\n unit: 'ms',\n },\n { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 0.7 },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n ],\n },\n {\n id: 'phaser',\n name: 'Phaser',\n category: 'modulation',\n description: 'Classic phaser effect using allpass filters',\n parameters: [\n {\n name: 'frequency',\n label: 'Rate',\n type: 'number',\n min: 0.1,\n max: 10,\n step: 0.1,\n default: 0.5,\n unit: 'Hz',\n },\n { name: 'octaves', label: 'Octaves', type: 'number', min: 1, max: 6, step: 1, default: 3 },\n {\n name: 'baseFrequency',\n label: 'Base Freq',\n type: 'number',\n min: 100,\n max: 2000,\n step: 10,\n default: 350,\n unit: 'Hz',\n },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n ],\n },\n {\n id: 'tremolo',\n name: 'Tremolo',\n category: 'modulation',\n description: 'Rhythmic volume modulation',\n parameters: [\n {\n name: 'frequency',\n label: 'Rate',\n type: 'number',\n min: 0.1,\n max: 20,\n step: 0.1,\n default: 4,\n unit: 'Hz',\n },\n { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n ],\n },\n {\n id: 'vibrato',\n name: 'Vibrato',\n category: 'modulation',\n description: 'Pitch modulation effect',\n parameters: [\n {\n name: 'frequency',\n label: 'Rate',\n type: 'number',\n min: 0.1,\n max: 20,\n step: 0.1,\n default: 5,\n unit: 'Hz',\n },\n { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 0.1 },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n ],\n },\n {\n id: 'autoPanner',\n name: 'Auto Panner',\n category: 'modulation',\n description: 'Automatic left-right panning',\n parameters: [\n {\n name: 'frequency',\n label: 'Rate',\n type: 'number',\n min: 0.1,\n max: 10,\n step: 0.1,\n default: 1,\n unit: 'Hz',\n },\n { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n ],\n },\n\n // === FILTER EFFECTS ===\n {\n id: 'autoFilter',\n name: 'Auto Filter',\n category: 'filter',\n description: 'Automated filter sweep with LFO',\n parameters: [\n {\n name: 'frequency',\n label: 'Rate',\n type: 'number',\n min: 0.1,\n max: 10,\n step: 0.1,\n default: 1,\n unit: 'Hz',\n },\n {\n name: 'baseFrequency',\n label: 'Base Freq',\n type: 'number',\n min: 20,\n max: 2000,\n step: 10,\n default: 200,\n unit: 'Hz',\n },\n {\n name: 'octaves',\n label: 'Octaves',\n type: 'number',\n min: 0.5,\n max: 8,\n step: 0.5,\n default: 2.6,\n },\n { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n ],\n },\n {\n id: 'autoWah',\n name: 'Auto Wah',\n category: 'filter',\n description: 'Envelope follower filter effect',\n parameters: [\n {\n name: 'baseFrequency',\n label: 'Base Freq',\n type: 'number',\n min: 20,\n max: 500,\n step: 10,\n default: 100,\n unit: 'Hz',\n },\n { name: 'octaves', label: 'Octaves', type: 'number', min: 1, max: 8, step: 1, default: 6 },\n {\n name: 'sensitivity',\n label: 'Sensitivity',\n type: 'number',\n min: -40,\n max: 0,\n step: 1,\n default: 0,\n unit: 'dB',\n },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n ],\n },\n {\n id: 'eq3',\n name: '3-Band EQ',\n category: 'filter',\n description: 'Three band equalizer with low, mid, and high controls',\n parameters: [\n {\n name: 'low',\n label: 'Low',\n type: 'number',\n min: -24,\n max: 24,\n step: 0.5,\n default: 0,\n unit: 'dB',\n },\n {\n name: 'mid',\n label: 'Mid',\n type: 'number',\n min: -24,\n max: 24,\n step: 0.5,\n default: 0,\n unit: 'dB',\n },\n {\n name: 'high',\n label: 'High',\n type: 'number',\n min: -24,\n max: 24,\n step: 0.5,\n default: 0,\n unit: 'dB',\n },\n {\n name: 'lowFrequency',\n label: 'Low Freq',\n type: 'number',\n min: 20,\n max: 500,\n step: 10,\n default: 400,\n unit: 'Hz',\n },\n {\n name: 'highFrequency',\n label: 'High Freq',\n type: 'number',\n min: 1000,\n max: 10000,\n step: 100,\n default: 2500,\n unit: 'Hz',\n },\n ],\n },\n\n // === DISTORTION EFFECTS ===\n {\n id: 'distortion',\n name: 'Distortion',\n category: 'distortion',\n description: 'Wave shaping distortion effect',\n parameters: [\n {\n name: 'distortion',\n label: 'Drive',\n type: 'number',\n min: 0,\n max: 1,\n step: 0.01,\n default: 0.4,\n },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n ],\n },\n {\n id: 'bitCrusher',\n name: 'Bit Crusher',\n category: 'distortion',\n description: 'Reduces bit depth for lo-fi digital texture',\n parameters: [\n { name: 'bits', label: 'Bits', type: 'number', min: 1, max: 16, step: 1, default: 4 },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n ],\n },\n {\n id: 'chebyshev',\n name: 'Chebyshev',\n category: 'distortion',\n description: 'Waveshaping distortion using Chebyshev polynomials',\n parameters: [\n { name: 'order', label: 'Order', type: 'number', min: 1, max: 100, step: 1, default: 50 },\n { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n ],\n },\n\n // === DYNAMICS EFFECTS ===\n {\n id: 'compressor',\n name: 'Compressor',\n category: 'dynamics',\n description: 'Dynamic range compressor',\n parameters: [\n {\n name: 'threshold',\n label: 'Threshold',\n type: 'number',\n min: -60,\n max: 0,\n step: 1,\n default: -24,\n unit: 'dB',\n },\n { name: 'ratio', label: 'Ratio', type: 'number', min: 1, max: 20, step: 0.5, default: 4 },\n {\n name: 'attack',\n label: 'Attack',\n type: 'number',\n min: 0,\n max: 1,\n step: 0.001,\n default: 0.003,\n unit: 's',\n },\n {\n name: 'release',\n label: 'Release',\n type: 'number',\n min: 0,\n max: 1,\n step: 0.01,\n default: 0.25,\n unit: 's',\n },\n {\n name: 'knee',\n label: 'Knee',\n type: 'number',\n min: 0,\n max: 40,\n step: 1,\n default: 30,\n unit: 'dB',\n },\n ],\n },\n {\n id: 'limiter',\n name: 'Limiter',\n category: 'dynamics',\n description: 'Hard limiter to prevent clipping',\n parameters: [\n {\n name: 'threshold',\n label: 'Threshold',\n type: 'number',\n min: -12,\n max: 0,\n step: 0.5,\n default: -6,\n unit: 'dB',\n },\n ],\n },\n {\n id: 'gate',\n name: 'Gate',\n category: 'dynamics',\n description: 'Noise gate to silence signal below threshold',\n parameters: [\n {\n name: 'threshold',\n label: 'Threshold',\n type: 'number',\n min: -100,\n max: 0,\n step: 1,\n default: -40,\n unit: 'dB',\n },\n {\n name: 'attack',\n label: 'Attack',\n type: 'number',\n min: 0,\n max: 0.3,\n step: 0.001,\n default: 0.001,\n unit: 's',\n },\n {\n name: 'release',\n label: 'Release',\n type: 'number',\n min: 0,\n max: 0.5,\n step: 0.01,\n default: 0.1,\n unit: 's',\n },\n ],\n },\n\n // === SPATIAL EFFECTS ===\n {\n id: 'stereoWidener',\n name: 'Stereo Widener',\n category: 'spatial',\n description: 'Expands or narrows the stereo image',\n parameters: [\n { name: 'width', label: 'Width', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n ],\n },\n];\n\n// Helper to get effect definition by ID\nexport const getEffectDefinition = (id: string): EffectDefinition | undefined => {\n return effectDefinitions.find((def) => def.id === id);\n};\n\n// Helper to get effects by category\nexport const getEffectsByCategory = (\n category: EffectDefinition['category']\n): EffectDefinition[] => {\n return effectDefinitions.filter((def) => def.category === category);\n};\n\n// All categories with their display names\nexport const effectCategories: { id: EffectDefinition['category']; name: string }[] = [\n { id: 'reverb', name: 'Reverb' },\n { id: 'delay', name: 'Delay' },\n { id: 'modulation', name: 'Modulation' },\n { id: 'filter', name: 'Filter' },\n { id: 'distortion', name: 'Distortion' },\n { id: 'dynamics', name: 'Dynamics' },\n { id: 'spatial', name: 'Spatial' },\n];\n","/**\n * Factory for creating Tone.js effect instances from effect definitions\n */\nimport {\n Reverb,\n Freeverb,\n JCReverb,\n FeedbackDelay,\n PingPongDelay,\n Chorus,\n Phaser,\n Tremolo,\n Vibrato,\n AutoPanner,\n AutoFilter,\n AutoWah,\n EQ3,\n Distortion,\n BitCrusher,\n Chebyshev,\n Compressor,\n Limiter,\n Gate,\n StereoWidener,\n ToneAudioNode,\n} from 'tone';\nimport type { InputNode } from 'tone';\nimport type { EffectDefinition } from './effectDefinitions';\n\n// Type for effect instance with common methods\nexport interface EffectInstance {\n effect: ToneAudioNode; // Tone.js effect instance\n id: string;\n instanceId: string;\n dispose: () => void;\n setParameter: (name: string, value: number | string | boolean) => void;\n getParameter: (name: string) => number | string | boolean | undefined;\n connect: (destination: InputNode) => void;\n disconnect: () => void;\n}\n\n// Each Tone.js effect constructor accepts different option types (ReverbOptions,\n// ChorusOptions, etc.) but all produce ToneAudioNode subclasses. We use a\n// permissive constructor signature to unify them in a single lookup map.\ntype EffectConstructor = new (options?: Record<string, number | string | boolean>) => ToneAudioNode;\n\n/** Centralizes the single unavoidable cast from a specific Tone.js effect constructor to EffectConstructor. */\nfunction asEffectConstructor(ctor: new (...args: never[]) => ToneAudioNode): EffectConstructor {\n return ctor as unknown as EffectConstructor;\n}\n\n// Map of effect IDs to their Tone.js constructors\nconst effectConstructors: Record<string, EffectConstructor> = {\n reverb: asEffectConstructor(Reverb),\n freeverb: asEffectConstructor(Freeverb),\n jcReverb: asEffectConstructor(JCReverb),\n feedbackDelay: asEffectConstructor(FeedbackDelay),\n pingPongDelay: asEffectConstructor(PingPongDelay),\n chorus: asEffectConstructor(Chorus),\n phaser: asEffectConstructor(Phaser),\n tremolo: asEffectConstructor(Tremolo),\n vibrato: asEffectConstructor(Vibrato),\n autoPanner: asEffectConstructor(AutoPanner),\n autoFilter: asEffectConstructor(AutoFilter),\n autoWah: asEffectConstructor(AutoWah),\n eq3: asEffectConstructor(EQ3),\n distortion: asEffectConstructor(Distortion),\n bitCrusher: asEffectConstructor(BitCrusher),\n chebyshev: asEffectConstructor(Chebyshev),\n compressor: asEffectConstructor(Compressor),\n limiter: asEffectConstructor(Limiter),\n gate: asEffectConstructor(Gate),\n stereoWidener: asEffectConstructor(StereoWidener),\n};\n\n// Generate unique instance ID\nlet instanceCounter = 0;\nconst generateInstanceId = (): string => {\n return `effect_${Date.now()}_${++instanceCounter}`;\n};\n\n/**\n * Create an effect instance from a definition with initial parameter values\n */\nexport function createEffectInstance(\n definition: EffectDefinition,\n initialParams?: Record<string, number | string | boolean>\n): EffectInstance {\n const Constructor = effectConstructors[definition.id];\n if (!Constructor) {\n throw new Error(`Unknown effect type: ${definition.id}`);\n }\n\n // Build initial options from definition defaults and any overrides\n const options: Record<string, number | string | boolean> = {};\n definition.parameters.forEach((param) => {\n const value = initialParams?.[param.name] ?? param.default;\n options[param.name] = value;\n });\n\n // Create the effect instance\n const effect = new Constructor(options);\n const instanceId = generateInstanceId();\n\n // Dynamic property access on Tone.js effects for parameter get/set.\n // Each effect type (Reverb, Chorus, EQ3, etc.) exposes different parameters as\n // properties or Signals. We cast to a record type for safe dynamic access.\n const effectRecord = effect as unknown as Record<string, { value?: unknown } | unknown>;\n\n return {\n effect,\n id: definition.id,\n instanceId,\n\n dispose() {\n try {\n effect.disconnect();\n effect.dispose();\n } catch (e) {\n console.warn(\n `[waveform-playlist] Error disposing effect \"${definition.id}\" (${instanceId}):`,\n e\n );\n }\n },\n\n setParameter(name: string, value: number | string | boolean) {\n // Handle special cases for different effect types\n const prop = effectRecord[name];\n if (name === 'wet') {\n const wetProp = effectRecord['wet'] as { value?: number } | undefined;\n if (wetProp && typeof wetProp === 'object' && 'value' in wetProp) {\n wetProp.value = value as number;\n return;\n }\n }\n if (prop !== undefined) {\n // Check if it's a Tone.js Signal (has .value property)\n if (prop && typeof prop === 'object' && 'value' in prop) {\n (prop as { value: unknown }).value = value;\n } else {\n effectRecord[name] = value;\n }\n }\n },\n\n getParameter(name: string): number | string | boolean | undefined {\n if (name === 'wet') {\n const wetProp = effectRecord['wet'] as { value?: number } | undefined;\n if (wetProp && typeof wetProp === 'object' && 'value' in wetProp) {\n return wetProp.value;\n }\n }\n const prop = effectRecord[name];\n if (prop !== undefined) {\n if (prop && typeof prop === 'object' && 'value' in prop) {\n return (prop as { value: unknown }).value as number | string | boolean;\n }\n return prop as number | string | boolean;\n }\n return undefined;\n },\n\n connect(destination: InputNode) {\n effect.connect(destination);\n },\n\n disconnect() {\n try {\n effect.disconnect();\n } catch (e) {\n console.warn(\n `[waveform-playlist] Error disconnecting effect \"${definition.id}\" (${instanceId}):`,\n e\n );\n }\n },\n };\n}\n\n/**\n * Create a chain of effects connected in series\n */\nexport function createEffectChain(effects: EffectInstance[]): {\n input: ToneAudioNode;\n output: ToneAudioNode;\n dispose: () => void;\n} {\n if (effects.length === 0) {\n throw new Error('Cannot create effect chain with no effects');\n }\n\n // Connect effects in series\n for (let i = 0; i < effects.length - 1; i++) {\n effects[i].effect.connect(effects[i + 1].effect);\n }\n\n return {\n input: effects[0].effect,\n output: effects[effects.length - 1].effect,\n dispose() {\n effects.forEach((e) => e.dispose());\n },\n };\n}\n","import { useState, useCallback, useRef, useEffect } from 'react';\nimport type { TrackEffectsFunction } from '../index';\nimport {\n effectDefinitions,\n getEffectDefinition,\n type EffectDefinition,\n} from '../effects/effectDefinitions';\nimport { createEffectInstance, type EffectInstance } from '../effects/effectFactory';\nimport { Gain, ToneAudioNode } from 'tone';\n\nexport interface TrackActiveEffect {\n instanceId: string;\n effectId: string;\n definition: EffectDefinition;\n params: Record<string, number | string | boolean>;\n bypassed: boolean;\n}\n\nexport interface TrackEffectsState {\n trackId: string;\n activeEffects: TrackActiveEffect[];\n}\n\nexport interface UseTrackDynamicEffectsReturn {\n // State per track\n trackEffectsState: Map<string, TrackActiveEffect[]>;\n\n // Actions\n addEffectToTrack: (trackId: string, effectId: string) => void;\n removeEffectFromTrack: (trackId: string, instanceId: string) => void;\n updateTrackEffectParameter: (\n trackId: string,\n instanceId: string,\n paramName: string,\n value: number | string | boolean\n ) => void;\n toggleBypass: (trackId: string, instanceId: string) => void;\n clearTrackEffects: (trackId: string) => void;\n getTrackEffectsFunction: (trackId: string) => TrackEffectsFunction | undefined;\n\n /**\n * Creates a fresh effects function for a track for offline rendering.\n * This creates new effect instances that work in the offline AudioContext.\n */\n createOfflineTrackEffectsFunction: (trackId: string) => TrackEffectsFunction | undefined;\n\n // Available effects\n availableEffects: EffectDefinition[];\n}\n\n/**\n * Hook for managing dynamic effects per track with real-time parameter updates\n */\nexport function useTrackDynamicEffects(): UseTrackDynamicEffectsReturn {\n // Track effects state per track (for UI)\n const [trackEffectsState, setTrackEffectsState] = useState<Map<string, TrackActiveEffect[]>>(\n new Map()\n );\n\n // Track effect instances per track (for audio processing)\n const trackEffectInstancesRef = useRef<Map<string, Map<string, EffectInstance>>>(new Map());\n\n // Track graph nodes per track for rebuilding chains\n const trackGraphNodesRef = useRef<\n Map<\n string,\n {\n graphEnd: Gain;\n masterGainNode: ToneAudioNode;\n }\n >\n >(new Map());\n\n // Rebuild the effect chain for a specific track\n // Note: trackEffects is passed as parameter to avoid stale closure issues\n const rebuildTrackChain = useCallback((trackId: string, trackEffects: TrackActiveEffect[]) => {\n const nodes = trackGraphNodesRef.current.get(trackId);\n if (!nodes) return;\n\n const { graphEnd, masterGainNode } = nodes;\n const instancesMap = trackEffectInstancesRef.current.get(trackId);\n\n // Disconnect everything first\n try {\n graphEnd.disconnect();\n } catch (e) {\n console.warn(`[waveform-playlist] Error disconnecting track \"${trackId}\" effect chain:`, e);\n }\n\n // Get effect instances in order\n const instances = trackEffects\n .map((ae) => instancesMap?.get(ae.instanceId))\n .filter((inst): inst is EffectInstance => inst !== undefined);\n\n if (instances.length === 0) {\n // No effects - connect directly\n graphEnd.connect(masterGainNode);\n } else {\n // Connect: graphEnd -> effect1 -> effect2 -> ... -> masterGainNode\n let currentNode: ToneAudioNode = graphEnd;\n\n instances.forEach((inst) => {\n try {\n inst.disconnect();\n } catch (e) {\n console.warn(\n `[waveform-playlist] Error disconnecting effect \"${inst.id}\" on track \"${trackId}\":`,\n e\n );\n }\n currentNode.connect(inst.effect);\n currentNode = inst.effect;\n });\n\n // Connect last effect to master\n currentNode.connect(masterGainNode);\n }\n }, []);\n\n // Add a new effect to a track\n const addEffectToTrack = useCallback((trackId: string, effectId: string) => {\n const definition = getEffectDefinition(effectId);\n if (!definition) {\n console.error(`Unknown effect: ${effectId}`);\n return;\n }\n\n // Build default params\n const params: Record<string, number | string | boolean> = {};\n definition.parameters.forEach((p) => {\n params[p.name] = p.default;\n });\n\n // Create the effect instance\n const instance = createEffectInstance(definition, params);\n\n // Initialize maps if needed\n if (!trackEffectInstancesRef.current.has(trackId)) {\n trackEffectInstancesRef.current.set(trackId, new Map());\n }\n trackEffectInstancesRef.current.get(trackId)!.set(instance.instanceId, instance);\n\n // Add to state\n const newActiveEffect: TrackActiveEffect = {\n instanceId: instance.instanceId,\n effectId: definition.id,\n definition,\n params,\n bypassed: false,\n };\n\n setTrackEffectsState((prev) => {\n const newState = new Map(prev);\n const existing = newState.get(trackId) || [];\n newState.set(trackId, [...existing, newActiveEffect]);\n return newState;\n });\n }, []);\n\n // Remove an effect from a track\n const removeEffectFromTrack = useCallback((trackId: string, instanceId: string) => {\n const instancesMap = trackEffectInstancesRef.current.get(trackId);\n const instance = instancesMap?.get(instanceId);\n if (instance) {\n instance.dispose();\n instancesMap?.delete(instanceId);\n }\n\n setTrackEffectsState((prev) => {\n const newState = new Map(prev);\n const existing = newState.get(trackId) || [];\n newState.set(\n trackId,\n existing.filter((e) => e.instanceId !== instanceId)\n );\n return newState;\n });\n }, []);\n\n // Update a parameter in real-time\n const updateTrackEffectParameter = useCallback(\n (trackId: string, instanceId: string, paramName: string, value: number | string | boolean) => {\n // Update the actual effect instance\n const instancesMap = trackEffectInstancesRef.current.get(trackId);\n const instance = instancesMap?.get(instanceId);\n if (instance) {\n instance.setParameter(paramName, value);\n }\n\n // Update state for UI\n setTrackEffectsState((prev) => {\n const newState = new Map(prev);\n const existing = newState.get(trackId) || [];\n newState.set(\n trackId,\n existing.map((e) =>\n e.instanceId === instanceId ? { ...e, params: { ...e.params, [paramName]: value } } : e\n )\n );\n return newState;\n });\n },\n []\n );\n\n // Toggle bypass for an effect (uses wet parameter - 0 = bypass, restore original for active)\n const toggleBypass = useCallback((trackId: string, instanceId: string) => {\n // Get current state from ref to determine new bypassed value (avoids stale closure)\n const trackEffects = trackEffectsStateRef.current.get(trackId) || [];\n const effect = trackEffects.find((e) => e.instanceId === instanceId);\n if (!effect) return;\n\n const newBypassed = !effect.bypassed;\n\n // Update the actual effect instance\n // When bypassing: set wet to 0\n // When un-bypassing: restore the original wet value from params\n const instancesMap = trackEffectInstancesRef.current.get(trackId);\n const instance = instancesMap?.get(instanceId);\n if (instance) {\n const originalWet = (effect.params.wet as number) ?? 1;\n instance.setParameter('wet', newBypassed ? 0 : originalWet);\n }\n\n // Update state for UI\n setTrackEffectsState((prev) => {\n const newState = new Map(prev);\n const existing = newState.get(trackId) || [];\n newState.set(\n trackId,\n existing.map((e) => (e.instanceId === instanceId ? { ...e, bypassed: newBypassed } : e))\n );\n return newState;\n });\n }, []);\n\n // Clear all effects from a track\n const clearTrackEffects = useCallback((trackId: string) => {\n // Dispose all instances for this track\n const instancesMap = trackEffectInstancesRef.current.get(trackId);\n if (instancesMap) {\n instancesMap.forEach((inst) => inst.dispose());\n instancesMap.clear();\n }\n\n setTrackEffectsState((prev) => {\n const newState = new Map(prev);\n newState.set(trackId, []);\n return newState;\n });\n }, []);\n\n // Ref to store the current trackEffectsState for reading in effects function\n // This avoids stale closure issues when the effects function is called later\n const trackEffectsStateRef = useRef<Map<string, TrackActiveEffect[]>>(trackEffectsState);\n trackEffectsStateRef.current = trackEffectsState;\n\n // Get the effects function for a track to pass to useAudioTracks\n // This function is stable (no dependencies) - it reads from refs at call time\n const getTrackEffectsFunction = useCallback(\n (trackId: string): TrackEffectsFunction | undefined => {\n // Return a function that connects effects when the track is loaded\n return (graphEnd, masterGainNode, _isOffline) => {\n // Store references for rebuilding chain\n trackGraphNodesRef.current.set(trackId, {\n graphEnd,\n masterGainNode,\n });\n\n // Read current state from ref (not stale closure)\n const trackEffects = trackEffectsStateRef.current.get(trackId) || [];\n const instancesMap = trackEffectInstancesRef.current.get(trackId);\n\n // Get effect instances in order\n const instances = trackEffects\n .map((ae) => instancesMap?.get(ae.instanceId))\n .filter((inst): inst is EffectInstance => inst !== undefined);\n\n if (instances.length === 0) {\n // No effects - connect directly\n graphEnd.connect(masterGainNode);\n } else {\n // Connect: graphEnd -> effect1 -> effect2 -> ... -> masterGainNode\n let currentNode: ToneAudioNode = graphEnd;\n\n instances.forEach((inst) => {\n currentNode.connect(inst.effect);\n currentNode = inst.effect;\n });\n\n // Connect last effect to master\n currentNode.connect(masterGainNode);\n }\n\n return function cleanup() {\n trackGraphNodesRef.current.delete(trackId);\n };\n };\n },\n [] // No dependencies - stable function that reads from refs\n );\n\n // Rebuild chains when effects change\n useEffect(() => {\n trackEffectsState.forEach((effects, trackId) => {\n rebuildTrackChain(trackId, effects);\n });\n }, [trackEffectsState, rebuildTrackChain]);\n\n // Cleanup on unmount\n useEffect(() => {\n const trackEffectInstances = trackEffectInstancesRef.current;\n return () => {\n trackEffectInstances.forEach((instancesMap) => {\n instancesMap.forEach((inst) => inst.dispose());\n instancesMap.clear();\n });\n trackEffectInstances.clear();\n };\n }, []);\n\n /**\n * Creates a fresh effects function for a track for offline rendering.\n * This creates new effect instances in the offline context, avoiding the\n * AudioContext mismatch issue that occurs when reusing real-time effects.\n */\n const createOfflineTrackEffectsFunction = useCallback(\n (trackId: string): TrackEffectsFunction | undefined => {\n const trackEffects = trackEffectsState.get(trackId) || [];\n // Get non-bypassed effects\n const nonBypassedEffects = trackEffects.filter((e) => !e.bypassed);\n\n if (nonBypassedEffects.length === 0) {\n return undefined;\n }\n\n // Return a function that creates fresh effect instances\n return (graphEnd: Gain, masterGainNode: ToneAudioNode, _isOffline: boolean) => {\n // Create fresh effect instances for offline context\n const offlineInstances: EffectInstance[] = [];\n\n for (const activeEffect of nonBypassedEffects) {\n const instance = createEffectInstance(activeEffect.definition, activeEffect.params);\n offlineInstances.push(instance);\n }\n\n if (offlineInstances.length === 0) {\n // No effects - connect directly\n graphEnd.connect(masterGainNode);\n } else {\n // Connect: graphEnd -> effect1 -> effect2 -> ... -> masterGainNode\n let currentNode: ToneAudioNode = graphEnd;\n\n offlineInstances.forEach((inst) => {\n currentNode.connect(inst.effect);\n currentNode = inst.effect;\n });\n\n // Connect last effect to master\n currentNode.connect(masterGainNode);\n }\n\n return function cleanup() {\n offlineInstances.forEach((inst) => inst.dispose());\n };\n };\n },\n [trackEffectsState]\n );\n\n return {\n trackEffectsState,\n addEffectToTrack,\n removeEffectFromTrack,\n updateTrackEffectParameter,\n toggleBypass,\n clearTrackEffects,\n getTrackEffectsFunction,\n createOfflineTrackEffectsFunction,\n availableEffects: effectDefinitions,\n };\n}\n","import { useState, useCallback } from 'react';\nimport {\n gainToDb,\n trackChannelCount,\n applyFadeIn,\n applyFadeOut,\n type ClipTrack,\n type FadeType,\n} from '@waveform-playlist/core';\nimport {\n type EffectsFunction,\n getUnderlyingAudioParam,\n getGlobalAudioContext,\n} from '@waveform-playlist/playout';\nimport { encodeWav, downloadBlob, type WavEncoderOptions } from '../utils/wavEncoder';\n\n/** Function type for per-track effects (same as in @waveform-playlist/core) */\nexport type TrackEffectsFunction = (\n graphEnd: unknown,\n destination: unknown,\n isOffline: boolean\n) => void | (() => void);\n\nexport interface ExportOptions extends WavEncoderOptions {\n /** Filename for download (without extension) */\n filename?: string;\n /** Export mode: 'master' for full mixdown, 'individual' for single track */\n mode?: 'master' | 'individual';\n /** Track index for individual export (only used when mode is 'individual') */\n trackIndex?: number;\n /** Whether to trigger automatic download */\n autoDownload?: boolean;\n /** Whether to apply effects (fades, etc.) - defaults to true */\n applyEffects?: boolean;\n /**\n * Optional Tone.js effects function for master effects. When provided, export renders\n * through the effects chain. The function receives isOffline=true.\n */\n effectsFunction?: EffectsFunction;\n /**\n * Optional function to create offline track effects.\n * Takes a trackId and returns a TrackEffectsFunction for offline rendering.\n * This is used instead of track.effects to avoid AudioContext mismatch issues.\n */\n createOfflineTrackEffects?: (trackId: string) => TrackEffectsFunction | undefined;\n /** Progress callback (0-1) */\n onProgress?: (progress: number) => void;\n}\n\nexport interface ExportResult {\n /** The rendered audio buffer */\n audioBuffer: AudioBuffer;\n /** The WAV file as a Blob */\n blob: Blob;\n /** Duration in seconds */\n duration: number;\n}\n\nexport interface UseExportWavReturn {\n /** Export the playlist to WAV */\n exportWav: (\n tracks: ClipTrack[],\n trackStates: TrackState[],\n options?: ExportOptions\n ) => Promise<ExportResult>;\n /** Whether export is in progress */\n isExporting: boolean;\n /** Export progress (0-1) */\n progress: number;\n /** Error message if export failed */\n error: string | null;\n}\n\ninterface TrackState {\n muted: boolean;\n soloed: boolean;\n volume: number;\n pan: number;\n}\n\n/**\n * Hook for exporting the waveform playlist to WAV format.\n * Uses Tone.Offline for non-real-time rendering, mirroring the live playback graph.\n */\nexport function useExportWav(): UseExportWavReturn {\n const [isExporting, setIsExporting] = useState(false);\n const [progress, setProgress] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const exportWav = useCallback(\n async (\n tracks: ClipTrack[],\n trackStates: TrackState[],\n options: ExportOptions = {}\n ): Promise<ExportResult> => {\n const {\n filename = 'export',\n mode = 'master',\n trackIndex,\n autoDownload = true,\n applyEffects = true,\n effectsFunction,\n createOfflineTrackEffects,\n bitDepth = 16,\n onProgress,\n } = options;\n\n setIsExporting(true);\n setProgress(0);\n setError(null);\n\n try {\n // Validate inputs\n if (tracks.length === 0) {\n throw new Error('No tracks to export');\n }\n\n if (\n mode === 'individual' &&\n (trackIndex === undefined || trackIndex < 0 || trackIndex >= tracks.length)\n ) {\n throw new Error('Invalid track index for individual export');\n }\n\n // Use AudioContext sample rate — the single source of truth for all audio\n const sampleRate = getGlobalAudioContext().sampleRate;\n\n // Calculate total duration from all clips (in samples)\n let totalDurationSamples = 0;\n for (const track of tracks) {\n for (const clip of track.clips) {\n const clipEndSample = clip.startSample + clip.durationSamples;\n totalDurationSamples = Math.max(totalDurationSamples, clipEndSample);\n }\n }\n\n // Add a small buffer at the end (0.1 seconds) to avoid cutting off\n totalDurationSamples += Math.round(sampleRate * 0.1);\n\n const duration = totalDurationSamples / sampleRate;\n\n // Determine which tracks to render\n const tracksToRender =\n mode === 'individual'\n ? [{ track: tracks[trackIndex!], state: trackStates[trackIndex!], index: trackIndex! }]\n : tracks.map((track, index) => ({ track, state: trackStates[index], index }));\n\n // Check for solo - if any track is soloed, only play soloed tracks.\n // Skip solo logic for individual export — the user explicitly chose the track.\n const hasSolo = mode === 'master' && trackStates.some((state) => state.soloed);\n\n const reportProgress = (p: number) => {\n setProgress(p);\n onProgress?.(p);\n };\n\n const renderedBuffer = await renderOffline(\n tracksToRender,\n hasSolo,\n duration,\n sampleRate,\n applyEffects,\n effectsFunction,\n createOfflineTrackEffects,\n reportProgress\n );\n\n reportProgress(0.9);\n\n // Encode to WAV\n const blob = encodeWav(renderedBuffer, { bitDepth });\n\n reportProgress(1);\n\n // Auto download if requested\n if (autoDownload) {\n const exportFilename =\n mode === 'individual' ? `${filename}_${tracks[trackIndex!].name}` : filename;\n downloadBlob(blob, `${exportFilename}.wav`);\n }\n\n return {\n audioBuffer: renderedBuffer,\n blob,\n duration,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Export failed';\n setError(message);\n throw err;\n } finally {\n setIsExporting(false);\n }\n },\n []\n );\n\n return {\n exportWav,\n isExporting,\n progress,\n error,\n };\n}\n\n/**\n * Render the playlist offline using Tone.Offline.\n * Mirrors the live playback graph: Player → fadeGain → trackVolume → trackPan → trackMute → masterVolume → destination.\n * Effects chains (master and per-track) are conditionally inserted when provided.\n */\nasync function renderOffline(\n tracksToRender: { track: ClipTrack; state: TrackState; index: number }[],\n hasSolo: boolean,\n duration: number,\n sampleRate: number,\n applyEffects: boolean,\n effectsFunction: EffectsFunction | undefined,\n createOfflineTrackEffects: ((trackId: string) => TrackEffectsFunction | undefined) | undefined,\n onProgress: (progress: number) => void\n): Promise<AudioBuffer> {\n const { Offline, Volume, Gain, Panner, Player, ToneAudioBuffer } = await import('tone');\n\n onProgress(0.1);\n\n // Derive output channel count from audible tracks only\n const audibleTracks = tracksToRender.filter(({ state }) => {\n if (state.muted && !state.soloed) return false;\n if (hasSolo && !state.soloed) return false;\n return true;\n });\n const outputChannels = audibleTracks.reduce(\n (max, { track }) => Math.max(max, trackChannelCount(track)),\n 1\n );\n\n let buffer;\n try {\n buffer = await Offline(\n async ({ transport, destination }) => {\n // Master volume at unity gain\n const masterVolume = new Volume(0);\n\n // Conditionally insert master effects chain\n if (effectsFunction && applyEffects) {\n effectsFunction(masterVolume, destination, true);\n } else {\n masterVolume.connect(destination);\n }\n\n for (const { track, state } of audibleTracks) {\n // Track-level nodes mirror ToneTrack: volume → pan → mute\n const trackVolume = new Volume(gainToDb(state.volume));\n // Match channelCount to source material — Tone.js Panner defaults to 1\n // which forces stereo→mono downmix. Use 2 only for stereo sources.\n const trackPan = new Panner({ pan: state.pan, channelCount: trackChannelCount(track) });\n const trackMute = new Gain(state.muted ? 0 : 1);\n\n // Conditionally insert per-track effects chain\n const trackEffects = createOfflineTrackEffects?.(track.id);\n if (trackEffects && applyEffects) {\n trackEffects(trackMute, masterVolume, true);\n } else {\n trackMute.connect(masterVolume);\n }\n\n // Connect track chain: trackVolume → trackPan → trackMute\n trackPan.connect(trackMute);\n trackVolume.connect(trackPan);\n\n // Schedule each clip\n for (const clip of track.clips) {\n const {\n audioBuffer,\n startSample,\n durationSamples,\n offsetSamples,\n gain: clipGain,\n fadeIn,\n fadeOut,\n } = clip;\n\n // Skip clips without audioBuffer (peaks-only clips can't be exported)\n if (!audioBuffer) {\n console.warn(\n '[waveform-playlist] Skipping clip \"' +\n (clip.name || clip.id) +\n '\" - no audioBuffer for export'\n );\n continue;\n }\n\n // Convert samples to seconds\n const startTime = startSample / sampleRate;\n const clipDuration = durationSamples / sampleRate;\n const offset = offsetSamples / sampleRate;\n\n // Create player and clip-level fade gain\n const toneBuffer = new ToneAudioBuffer(audioBuffer);\n const player = new Player(toneBuffer);\n const fadeGain = new Gain(clipGain);\n\n // Connect: player → fadeGain → trackVolume\n player.connect(fadeGain);\n fadeGain.connect(trackVolume);\n\n // Apply fade automation via native AudioParam\n if (applyEffects) {\n const audioParam = getUnderlyingAudioParam(fadeGain.gain);\n if (audioParam) {\n applyClipFades(audioParam, clipGain, startTime, clipDuration, fadeIn, fadeOut);\n } else if (fadeIn || fadeOut) {\n console.warn(\n '[waveform-playlist] Cannot apply fades for clip \"' +\n (clip.name || clip.id) +\n '\" - AudioParam not accessible'\n );\n }\n }\n\n player.start(startTime, offset, clipDuration);\n }\n }\n\n transport.start(0);\n },\n duration,\n outputChannels,\n sampleRate\n );\n } catch (err) {\n if (err instanceof Error) {\n throw err;\n } else {\n throw new Error('Tone.Offline rendering failed: ' + String(err));\n }\n }\n\n onProgress(0.9);\n\n const result = buffer.get();\n if (!result) {\n throw new Error('Offline rendering produced no audio buffer');\n }\n return result;\n}\n\n/**\n * Apply fade in/out automation to a clip's gain AudioParam.\n * Delegates to core's applyFadeIn/applyFadeOut for consistent curves\n * between live playback and offline export.\n */\nfunction applyClipFades(\n gainParam: AudioParam,\n clipGain: number,\n startTime: number,\n clipDuration: number,\n fadeIn: { duration: number; type?: FadeType } | undefined,\n fadeOut: { duration: number; type?: FadeType } | undefined\n): void {\n // Set initial gain (0 if fade in, clipGain otherwise)\n if (fadeIn) {\n gainParam.setValueAtTime(0, startTime);\n } else {\n gainParam.setValueAtTime(clipGain, startTime);\n }\n\n if (fadeIn) {\n applyFadeIn(gainParam, startTime, fadeIn.duration, fadeIn.type || 'linear', 0, clipGain);\n }\n\n if (fadeOut) {\n const fadeOutStart = startTime + clipDuration - fadeOut.duration;\n // Ensure we're at clipGain before fade out starts\n if (!fadeIn || fadeIn.duration < clipDuration - fadeOut.duration) {\n gainParam.setValueAtTime(clipGain, fadeOutStart);\n }\n applyFadeOut(gainParam, fadeOutStart, fadeOut.duration, fadeOut.type || 'linear', clipGain, 0);\n }\n}\n\n/**\n * Export types\n */\nexport type { WavEncoderOptions };\n","/**\n * WAV file encoder\n * Converts AudioBuffer to WAV format Blob\n */\n\nexport interface WavEncoderOptions {\n /** Bit depth: 16 or 32. Default: 16 */\n bitDepth?: 16 | 32;\n}\n\n/**\n * Encode an AudioBuffer to WAV format\n * @param audioBuffer - The AudioBuffer to encode\n * @param options - Encoding options\n * @returns WAV file as Blob\n */\nexport function encodeWav(audioBuffer: AudioBuffer, options: WavEncoderOptions = {}): Blob {\n const { bitDepth = 16 } = options;\n\n const numChannels = audioBuffer.numberOfChannels;\n const sampleRate = audioBuffer.sampleRate;\n const numSamples = audioBuffer.length;\n const bytesPerSample = bitDepth / 8;\n const blockAlign = numChannels * bytesPerSample;\n const byteRate = sampleRate * blockAlign;\n const dataSize = numSamples * blockAlign;\n\n // WAV header is 44 bytes\n const headerSize = 44;\n const totalSize = headerSize + dataSize;\n\n const buffer = new ArrayBuffer(totalSize);\n const view = new DataView(buffer);\n\n // Write WAV header\n // RIFF chunk descriptor\n writeString(view, 0, 'RIFF');\n view.setUint32(4, totalSize - 8, true); // File size minus RIFF header\n writeString(view, 8, 'WAVE');\n\n // fmt sub-chunk\n writeString(view, 12, 'fmt ');\n view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM)\n view.setUint16(20, bitDepth === 32 ? 3 : 1, true); // AudioFormat (1=PCM, 3=IEEE float)\n view.setUint16(22, numChannels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, byteRate, true);\n view.setUint16(32, blockAlign, true);\n view.setUint16(34, bitDepth, true);\n\n // data sub-chunk\n writeString(view, 36, 'data');\n view.setUint32(40, dataSize, true);\n\n // Write interleaved audio data\n const channelData: Float32Array[] = [];\n for (let ch = 0; ch < numChannels; ch++) {\n channelData.push(audioBuffer.getChannelData(ch));\n }\n\n let offset = headerSize;\n\n if (bitDepth === 16) {\n // 16-bit PCM\n for (let i = 0; i < numSamples; i++) {\n for (let ch = 0; ch < numChannels; ch++) {\n const sample = channelData[ch][i];\n // Clamp to [-1, 1] and convert to 16-bit signed integer\n const clampedSample = Math.max(-1, Math.min(1, sample));\n const intSample = clampedSample < 0 ? clampedSample * 0x8000 : clampedSample * 0x7fff;\n view.setInt16(offset, intSample, true);\n offset += 2;\n }\n }\n } else {\n // 32-bit IEEE float\n for (let i = 0; i < numSamples; i++) {\n for (let ch = 0; ch < numChannels; ch++) {\n view.setFloat32(offset, channelData[ch][i], true);\n offset += 4;\n }\n }\n }\n\n return new Blob([buffer], { type: 'audio/wav' });\n}\n\n/**\n * Write a string to a DataView at the specified offset\n */\nfunction writeString(view: DataView, offset: number, str: string): void {\n for (let i = 0; i < str.length; i++) {\n view.setUint8(offset + i, str.charCodeAt(i));\n }\n}\n\n/**\n * Trigger a download of a Blob with the specified filename\n */\nexport function downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n","/**\n * useDynamicTracks — imperative hook for runtime track additions.\n *\n * Complements `useAudioTracks` (declarative, configs-driven) with an\n * imperative `addTracks()` API for dynamic loading (drag-and-drop, file pickers).\n *\n * Placeholder tracks appear instantly while audio decodes in parallel.\n */\n\nimport { useState, useCallback, useRef, useEffect } from 'react';\nimport { ClipTrack, createTrack, createClipFromSeconds } from '@waveform-playlist/core';\nimport { getGlobalAudioContext } from '@waveform-playlist/playout';\n\n/** A source that can be decoded into a track. */\nexport type TrackSource = File | Blob | string | { src: string; name?: string };\n\n/** Info about a track that failed to load. */\nexport interface TrackLoadError {\n /** Display name of the source that failed. */\n name: string;\n /** The underlying error. */\n error: Error;\n}\n\nexport interface UseDynamicTracksReturn {\n /**\n * Current tracks array (placeholders + loaded). Pass to WaveformPlaylistProvider.\n * Placeholder tracks have `clips: []` and names ending with \" (loading...)\".\n */\n tracks: ClipTrack[];\n /** Add one or more sources — creates placeholder tracks immediately. */\n addTracks: (sources: TrackSource[]) => void;\n /** Remove a track by its id. Aborts in-flight fetch/decode if still loading. */\n removeTrack: (trackId: string) => void;\n /** Number of sources currently decoding. */\n loadingCount: number;\n /** True when any source is still decoding. */\n isLoading: boolean;\n /** Tracks that failed to load (removed from `tracks` automatically). */\n errors: TrackLoadError[];\n}\n\n/** Extract a display name from a TrackSource. */\nfunction getSourceName(source: TrackSource): string {\n if (source instanceof File) {\n return source.name.replace(/\\.[^/.]+$/, '');\n }\n if (source instanceof Blob) {\n return 'Untitled';\n }\n if (typeof source === 'string') {\n return (\n source\n .split('/')\n .pop()\n ?.replace(/\\.[^/.]+$/, '') ?? 'Untitled'\n );\n }\n return (\n source.name ??\n source.src\n .split('/')\n .pop()\n ?.replace(/\\.[^/.]+$/, '') ??\n 'Untitled'\n );\n}\n\n/** Decode a TrackSource into an AudioBuffer + clean name. */\nasync function decodeSource(\n source: TrackSource,\n audioContext: AudioContext,\n signal?: AbortSignal\n): Promise<{ audioBuffer: AudioBuffer; name: string }> {\n const name = getSourceName(source);\n\n // File and Blob: read arrayBuffer directly (not abortable, but we check signal after)\n if (source instanceof Blob) {\n const arrayBuffer = await source.arrayBuffer();\n signal?.throwIfAborted();\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n return { audioBuffer, name };\n }\n\n const url = typeof source === 'string' ? source : source.src;\n const response = await fetch(url, { signal });\n if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);\n const arrayBuffer = await response.arrayBuffer();\n signal?.throwIfAborted();\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n return { audioBuffer, name };\n}\n\nexport function useDynamicTracks(): UseDynamicTracksReturn {\n const [tracks, setTracks] = useState<ClipTrack[]>([]);\n const [loadingCount, setLoadingCount] = useState(0);\n const [errors, setErrors] = useState<TrackLoadError[]>([]);\n const cancelledRef = useRef(false);\n /** Track IDs currently decoding — for accurate loadingCount on removal. */\n const loadingIdsRef = useRef(new Set<string>());\n /** Per-track AbortControllers for in-flight fetches — keyed by track ID. */\n const abortControllersRef = useRef(new Map<string, AbortController>());\n\n useEffect(() => {\n const controllers = abortControllersRef.current;\n return () => {\n cancelledRef.current = true;\n for (const controller of controllers.values()) {\n controller.abort();\n }\n controllers.clear();\n };\n }, []);\n\n const addTracks = useCallback((sources: TrackSource[]) => {\n if (sources.length === 0) return;\n\n const audioContext = getGlobalAudioContext();\n\n // 1. Create placeholder tracks immediately\n const placeholders = sources.map((source) => ({\n track: createTrack({ name: `${getSourceName(source)} (loading...)`, clips: [] }),\n source,\n }));\n\n setTracks((prev) => [...prev, ...placeholders.map((p) => p.track)]);\n setLoadingCount((prev) => prev + sources.length);\n\n // 2. Decode each source in parallel (fire-and-forget per source)\n for (const { track, source } of placeholders) {\n loadingIdsRef.current.add(track.id);\n const controller = new AbortController();\n abortControllersRef.current.set(track.id, controller);\n\n (async () => {\n try {\n const { audioBuffer, name } = await decodeSource(source, audioContext, controller.signal);\n const clip = createClipFromSeconds({\n audioBuffer,\n startTime: 0,\n duration: audioBuffer.duration,\n offset: 0,\n name,\n });\n\n // Guard: skip state update if hook unmounted or track was removed while decoding\n if (!cancelledRef.current && loadingIdsRef.current.has(track.id)) {\n setTracks((prev) =>\n prev.map((t) => (t.id === track.id ? { ...t, name, clips: [clip] } : t))\n );\n }\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') return;\n console.warn('[waveform-playlist] Error loading audio:', error);\n // Guard: skip state update if hook unmounted or track was removed while decoding\n if (!cancelledRef.current && loadingIdsRef.current.has(track.id)) {\n setTracks((prev) => prev.filter((t) => t.id !== track.id));\n setErrors((prev) => [\n ...prev,\n {\n name: getSourceName(source),\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ]);\n }\n } finally {\n abortControllersRef.current.delete(track.id);\n if (!cancelledRef.current && loadingIdsRef.current.delete(track.id)) {\n setLoadingCount((prev) => prev - 1);\n }\n }\n })();\n }\n }, []);\n\n const removeTrack = useCallback((trackId: string) => {\n setTracks((prev) => prev.filter((t) => t.id !== trackId));\n // Abort in-flight fetch/decode and update loading state\n const controller = abortControllersRef.current.get(trackId);\n if (controller) {\n controller.abort();\n abortControllersRef.current.delete(trackId);\n }\n if (loadingIdsRef.current.delete(trackId)) {\n setLoadingCount((prev) => prev - 1);\n }\n }, []);\n\n return {\n tracks,\n addTracks,\n removeTrack,\n loadingCount,\n isLoading: loadingCount > 0,\n errors,\n };\n}\n","/**\n * Hook for monitoring master output levels\n *\n * Connects an AudioWorklet meter processor to the Destination node for\n * real-time output level monitoring. Computes sample-accurate peak and\n * RMS via the meter worklet — no transient is missed.\n *\n * IMPORTANT: Uses getGlobalContext() from playout to ensure the meter\n * is created on the same AudioContext as the audio engine. Tone.js's\n * getContext()/getDestination() return the DEFAULT context, which is\n * replaced when getGlobalContext() calls setContext() on first audio init.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseOutputMeterOptions {\n /**\n * Number of channels to meter.\n * Default: 2 (stereo output)\n */\n channelCount?: number;\n\n /**\n * How often to update the levels (in Hz).\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Whether audio is currently playing. When this transitions to false,\n * all levels (current, peak, RMS) and smoothed state are reset to zero.\n * Without this, the browser's tail-time optimization stops calling the\n * worklet's process() when no audio flows, leaving the last non-zero\n * levels frozen in state.\n * Default: false\n */\n isPlaying?: boolean;\n}\n\nexport interface UseOutputMeterReturn {\n /** Per-channel peak output levels (0-1) */\n levels: number[];\n /** Per-channel held peak levels (0-1) */\n peakLevels: number[];\n /** Per-channel RMS output levels (0-1) */\n rmsLevels: number[];\n /** Reset all held peak levels to 0 */\n resetPeak: () => void;\n /** Error from meter setup (worklet load failure, context issues, etc.) */\n error: Error | null;\n}\n\nexport function useOutputMeter(options: UseOutputMeterOptions = {}): UseOutputMeterReturn {\n const { channelCount = 2, updateRate = 60, isPlaying = false } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n // Reset all levels when playback stops\n useEffect(() => {\n if (!isPlaying) {\n const zeros = new Array(channelCount).fill(0);\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n setLevels(zeros);\n setRmsLevels(zeros);\n setPeakLevels(zeros);\n }\n }, [isPlaying, channelCount]);\n\n useEffect(() => {\n let isMounted = true;\n\n const setup = async () => {\n // Use getGlobalContext() to ensure we're on the SAME context as the audio engine.\n const context = getGlobalContext();\n\n // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: channelCount,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Output meter worklet processor error:', String(event));\n };\n\n // Insert as pass-through in destination chain:\n // Volume → WorkletNode → Gain → rawContext.destination\n const destination = context.destination;\n destination.chain(workletNode);\n\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n // Smoothed peak: jump up instantly, decay slowly\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n setLevels(peakValues);\n setRmsLevels(rmsValues);\n setPeakLevels((prev) => peakValues.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setup().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up output meter:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (workletNodeRef.current) {\n // Restore default chain: Volume → Gain (removes worklet from path)\n try {\n const context = getGlobalContext();\n context.destination.chain();\n } catch (err) {\n console.warn('[waveform-playlist] Failed to restore destination chain:', String(err));\n }\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Output meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [channelCount, updateRate]);\n\n return { levels, peakLevels, rmsLevels, resetPeak, error: meterError };\n}\n","import React from 'react';\nimport { BaseControlButton } from '@waveform-playlist/ui-components';\nimport type { EffectsFunction } from '@waveform-playlist/playout';\nimport { usePlaylistData } from '../WaveformPlaylistContext';\nimport { useExportWav, type TrackEffectsFunction } from '../hooks/useExportWav';\n\nexport interface ExportWavButtonProps {\n /** Button label */\n label?: string;\n /** Filename for the downloaded file (without extension) */\n filename?: string;\n /** Export mode: 'master' for stereo mix, 'individual' for single track */\n mode?: 'master' | 'individual';\n /** Track index for individual export */\n trackIndex?: number;\n /** Bit depth: 16 or 32 */\n bitDepth?: 16 | 32;\n /** Whether to apply effects (fades, etc.) - defaults to true */\n applyEffects?: boolean;\n /**\n * Optional Tone.js effects function for master effects. When provided, export will use Tone.Offline\n * to render through the effects chain. The function receives isOffline=true.\n */\n effectsFunction?: EffectsFunction;\n /**\n * Optional function to create offline track effects.\n * Takes a trackId and returns a TrackEffectsFunction for offline rendering.\n */\n createOfflineTrackEffects?: (trackId: string) => TrackEffectsFunction | undefined;\n /** CSS class name */\n className?: string;\n /** Callback when export completes */\n onExportComplete?: (blob: Blob) => void;\n /** Callback when export fails */\n onExportError?: (error: Error) => void;\n}\n\nexport const ExportWavButton: React.FC<ExportWavButtonProps> = ({\n label = 'Export WAV',\n filename = 'export',\n mode = 'master',\n trackIndex,\n bitDepth = 16,\n applyEffects = true,\n effectsFunction,\n createOfflineTrackEffects,\n className,\n onExportComplete,\n onExportError,\n}) => {\n const { tracks, trackStates } = usePlaylistData();\n const { exportWav, isExporting, progress } = useExportWav();\n\n const handleExport = async () => {\n try {\n const result = await exportWav(tracks, trackStates, {\n filename,\n mode,\n trackIndex,\n bitDepth,\n applyEffects,\n effectsFunction,\n createOfflineTrackEffects,\n autoDownload: true,\n });\n onExportComplete?.(result.blob);\n } catch (error) {\n onExportError?.(error instanceof Error ? error : new Error('Export failed'));\n }\n };\n\n const buttonLabel = isExporting ? `Exporting ${Math.round(progress * 100)}%` : label;\n\n return (\n <BaseControlButton\n onClick={handleExport}\n disabled={isExporting || tracks.length === 0}\n className={className}\n >\n {buttonLabel}\n </BaseControlButton>\n );\n};\n","import React, {\n createContext,\n useContext,\n useState,\n useEffect,\n useRef,\n useCallback,\n useMemo,\n type ReactNode,\n} from 'react';\nimport { ThemeProvider } from 'styled-components';\nimport type {\n EffectsFunction,\n TrackEffectsFunction,\n SoundFontCache,\n} from '@waveform-playlist/playout';\nimport { resolvePlayoutAdapter } from './playout/resolvePlayoutAdapter';\nimport { PlaylistEngine, type EngineState, type PlayoutAdapter } from '@waveform-playlist/engine';\nimport {\n type ClipTrack,\n type Fade,\n type AnnotationAction,\n type MidiNoteData,\n} from '@waveform-playlist/core';\nimport {\n type TimeFormat,\n type WaveformPlaylistTheme,\n defaultTheme,\n} from '@waveform-playlist/ui-components';\nimport { extractPeaksFromWaveformDataFull } from './waveformDataLoader';\nimport { syncSoundFontCacheToAdapter } from './soundFontSync';\nimport type WaveformData from 'waveform-data';\nimport type { PeakData } from '@waveform-playlist/core';\nimport type { AnnotationData } from '@waveform-playlist/core';\nimport {\n useTimeFormat,\n useZoomControls,\n useMasterVolume,\n useSelectionState,\n useLoopState,\n useSelectedTrack,\n useUndoState,\n useAnimationFrameLoop,\n useWaveformDataCache,\n} from './hooks';\n\n// Types\nexport interface ClipPeaks {\n clipId: string;\n trackName: string;\n peaks: PeakData;\n startSample: number;\n durationSamples: number;\n fadeIn?: Fade;\n fadeOut?: Fade;\n midiNotes?: MidiNoteData[];\n sampleRate?: number;\n offsetSamples?: number;\n}\n\nexport type TrackClipPeaks = ClipPeaks[];\n\n// Legacy WaveformTrack type - kept for reference but deprecated\n// @deprecated Use ClipTrack from @waveform-playlist/core instead\nexport interface WaveformTrack {\n src: string | AudioBuffer;\n name?: string;\n effects?: TrackEffectsFunction;\n}\n\nexport interface TrackState {\n name: string;\n muted: boolean;\n soloed: boolean;\n volume: number;\n pan: number;\n}\n\n// Split contexts for performance optimization\n// Animation context contains playback state and timing refs — no per-frame state updates\n\n/** Per-frame data passed to registered animation callbacks. */\nexport interface FrameData {\n /** Raw engine time (for state/logic — NOT for visual positioning). */\n readonly time: number;\n /** Visually-aligned time for DOM positioning: engine.getAudibleTime() while\n * playing (matches speaker output), raw time when resting. */\n readonly visualTime: number;\n readonly sampleRate: number;\n readonly samplesPerPixel: number;\n}\n\nexport interface PlaybackAnimationContextValue {\n isPlaying: boolean;\n currentTime: number;\n currentTimeRef: React.RefObject<number>;\n /**\n * Visually-aligned playback time (engine.getAudibleTime() while playing;\n * raw resting time otherwise). Kept current by the animation loop during playback\n * and by pause/seek/stop paths when stopped. Read from this for any visual\n * positioning that should match the audible output.\n */\n visualTimeRef: React.RefObject<number>;\n // Refs for direct time calculation in animated components (avoids timing drift)\n playbackStartTimeRef: React.RefObject<number>; // context.currentTime when playback started\n audioStartPositionRef: React.RefObject<number>; // Audio position when playback started\n /** Returns raw playback time from engine (auto-wraps at loop boundaries). */\n getPlaybackTime: () => number;\n /** Current time of the adapter's AudioContext, in seconds. */\n getAudioContextTime: () => number;\n /**\n * Returns the current adapter scheduler lookahead (Tone ~0.1s, native 0).\n * Use this for any audible-latency calculation that must match the playhead\n * (e.g., recording live-preview peak slicing).\n */\n getLookAhead: () => number;\n /**\n * Returns the adapter AudioContext's output latency in seconds.\n * Use this for audible-latency calculations (e.g., recording live-preview peak slicing).\n */\n getOutputLatency: () => number;\n /** Register a per-frame callback driven by the single animation loop. */\n registerFrameCallback: (id: string, cb: (data: FrameData) => void) => void;\n /** Unregister a per-frame callback. */\n unregisterFrameCallback: (id: string) => void;\n}\n\nexport interface PlaylistStateContextValue {\n continuousPlay: boolean;\n linkEndpoints: boolean;\n annotationsEditable: boolean;\n isAutomaticScroll: boolean;\n isLoopEnabled: boolean;\n annotations: AnnotationData[];\n activeAnnotationId: string | null;\n selectionStart: number;\n selectionEnd: number;\n selectedTrackId: string | null; // ID of currently selected track for editing operations\n // Loop region (separate from selection) - Audacity-style loop points\n loopStart: number;\n loopEnd: number;\n /** Whether playback continues past the end of loaded audio */\n indefinitePlayback: boolean;\n /** Whether undo is available */\n canUndo: boolean;\n /** Whether redo is available */\n canRedo: boolean;\n}\n\nexport interface PlaylistControlsContextValue {\n // Playback controls\n play: (startTime?: number, playDuration?: number) => Promise<void>;\n pause: () => void;\n stop: () => void;\n seekTo: (time: number) => void;\n setCurrentTime: (time: number) => void;\n\n // Track controls\n setTrackMute: (trackIndex: number, muted: boolean) => void;\n setTrackSolo: (trackIndex: number, soloed: boolean) => void;\n setTrackVolume: (trackIndex: number, volume: number) => void;\n setTrackPan: (trackIndex: number, pan: number) => void;\n\n // Selection\n setSelection: (start: number, end: number) => void;\n setSelectedTrackId: (trackId: string | null) => void;\n\n // Time format\n setTimeFormat: (format: TimeFormat) => void;\n formatTime: (seconds: number) => string;\n\n // Zoom\n zoomIn: () => void;\n zoomOut: () => void;\n\n // Master volume\n setMasterVolume: (volume: number) => void;\n\n // Automatic scroll\n setAutomaticScroll: (enabled: boolean) => void;\n setScrollContainer: (element: HTMLDivElement | null) => void;\n scrollContainerRef: React.RefObject<HTMLDivElement | null>;\n\n // Annotation controls\n setContinuousPlay: (enabled: boolean) => void;\n setLinkEndpoints: (enabled: boolean) => void;\n setAnnotationsEditable: (enabled: boolean) => void;\n setAnnotations: React.Dispatch<React.SetStateAction<AnnotationData[]>>;\n setActiveAnnotationId: (id: string | null) => void;\n\n // Loop controls\n setLoopEnabled: (enabled: boolean) => void;\n setLoopRegion: (start: number, end: number) => void;\n setLoopRegionFromSelection: () => void;\n clearLoopRegion: () => void;\n\n // Undo/redo\n undo: () => void;\n redo: () => void;\n}\n\nexport interface PlaylistDataContextValue {\n duration: number;\n audioBuffers: AudioBuffer[];\n peaksDataArray: TrackClipPeaks[]; // Array of tracks, each containing array of clip peaks\n trackStates: TrackState[];\n tracks: ClipTrack[]; // Original tracks array with IDs\n sampleRate: number;\n waveHeight: number;\n timeScaleHeight: number;\n minimumPlaylistHeight: number;\n controls: { show: boolean; width: number };\n playoutRef: React.RefObject<PlaylistEngine | null>;\n samplesPerPixel: number;\n timeFormat: TimeFormat;\n masterVolume: number;\n canZoomIn: boolean;\n canZoomOut: boolean;\n barWidth: number;\n barGap: number;\n /** Width in pixels of progress bars. Defaults to barWidth + barGap (fills gaps). */\n progressBarWidth: number;\n /** Whether the playlist has finished loading all tracks */\n isReady: boolean;\n /** Whether tracks are rendered in mono mode */\n mono: boolean;\n /** Ref set by useClipDragHandlers during boundary trim drags.\n * When true, loadAudio skips engine rebuild — visual updates flow via React state only. */\n isDraggingRef: React.MutableRefObject<boolean>;\n onTracksChange: ((tracks: ClipTrack[]) => void) | undefined;\n}\n\n// Create the 4 separate contexts\nconst PlaybackAnimationContext = createContext<PlaybackAnimationContextValue | null>(null);\nconst PlaylistStateContext = createContext<PlaylistStateContextValue | null>(null);\nconst PlaylistControlsContext = createContext<PlaylistControlsContextValue | null>(null);\nconst PlaylistDataContext = createContext<PlaylistDataContextValue | null>(null);\n\nexport interface WaveformPlaylistProviderProps {\n tracks: ClipTrack[]; // Updated to use clip-based model\n timescale?: boolean;\n mono?: boolean;\n waveHeight?: number;\n samplesPerPixel?: number;\n zoomLevels?: number[]; // Array of zoom levels in samples per pixel (lower = more zoomed in)\n automaticScroll?: boolean;\n theme?: Partial<WaveformPlaylistTheme>;\n controls?: {\n show: boolean;\n width: number;\n };\n annotationList?: {\n annotations?: AnnotationData[];\n editable?: boolean;\n isContinuousPlay?: boolean;\n linkEndpoints?: boolean;\n controls?: AnnotationAction[];\n };\n effects?: EffectsFunction;\n onReady?: () => void;\n /** Called when audio/engine initialization fails (e.g. a missing optional peer,\n * a throwing `createAdapter`, or an invalid `zoomLevels`/`samplesPerPixel`). The\n * provider already logs the error; use this to surface it in your UI. */\n onError?: (err: Error) => void;\n /** @deprecated Use onAnnotationsChange instead */\n onAnnotationUpdate?: (annotations: AnnotationData[]) => void;\n /** Callback when annotations are changed (drag, edit, etc.) */\n onAnnotationsChange?: (annotations: AnnotationData[]) => void;\n /** Width in pixels of waveform bars. Default: 1 */\n barWidth?: number;\n /** Spacing in pixels between waveform bars. Default: 0 */\n barGap?: number;\n /** Width in pixels of progress bars. Default: barWidth + barGap (fills gaps). */\n progressBarWidth?: number;\n /** Callback when engine clip operations (move, trim, split) change tracks.\n * The provider calls this so the parent can update its tracks state without\n * triggering a full engine rebuild.\n *\n * **Important:** The parent must pass the received `tracks` reference back as\n * the `tracks` prop (i.e. `setState(tracks)`). The provider uses reference\n * identity (`tracks === engineTracksRef.current`) to detect engine-originated\n * updates and skip the expensive `loadAudio` rebuild. */\n onTracksChange?: (tracks: ClipTrack[]) => void;\n /** SoundFont cache for sample-based MIDI playback. When provided, MIDI clips\n * use SoundFont samples instead of PolySynth synthesis. */\n soundFontCache?: SoundFontCache;\n /** When true, tracks render visually but the engine build is deferred.\n * Use this during progressive loading to avoid rebuilding the engine for\n * each track — flip to false when all tracks are ready for a single build. */\n deferEngineRebuild?: boolean;\n /** Disable automatic stop when the cursor reaches the end of the longest\n * track. Useful for DAW-style recording beyond existing audio. */\n indefinitePlayback?: boolean;\n /** Desired AudioContext sample rate. Creates a cross-browser AudioContext at\n * this rate via standardized-audio-context. Pre-computed peaks (.dat files)\n * render instantly when they match. On mismatch, falls back to worker. */\n sampleRate?: number;\n /** Factory for a custom PlayoutAdapter. When omitted, the Tone.js engine\n * (@waveform-playlist/playout) is dynamically imported — so a custom adapter\n * lets a consumer use neither @waveform-playlist/playout nor tone. Called once\n * per engine rebuild; the provider owns and disposes the returned instance.\n * Pass a stable reference (module-level or useCallback) — it is read directly\n * inside the curated `loadAudio` effect, not via its dependency array. */\n createAdapter?: () => PlayoutAdapter;\n children: ReactNode;\n}\n\nexport const WaveformPlaylistProvider: React.FC<WaveformPlaylistProviderProps> = ({\n tracks,\n timescale = false,\n mono = false,\n waveHeight = 80,\n samplesPerPixel: initialSamplesPerPixel = 1024,\n zoomLevels,\n automaticScroll = false,\n theme: userTheme,\n controls = { show: false, width: 0 },\n annotationList,\n effects,\n onReady,\n onError,\n onAnnotationUpdate: _onAnnotationUpdate,\n onAnnotationsChange,\n barWidth = 1,\n barGap = 0,\n progressBarWidth: progressBarWidthProp,\n onTracksChange,\n soundFontCache,\n deferEngineRebuild = false,\n indefinitePlayback = false,\n sampleRate: sampleRateProp,\n createAdapter,\n children,\n}) => {\n // Default progressBarWidth to barWidth + barGap (fills gaps)\n const progressBarWidth = progressBarWidthProp ?? barWidth + barGap;\n\n // Ref for animation loop access (avoids adding prop to useCallback deps)\n const indefinitePlaybackRef = useRef(indefinitePlayback);\n indefinitePlaybackRef.current = indefinitePlayback;\n\n // Stabilize zoomLevels reference — inline arrays (e.g. zoomLevels={[256, 512]})\n // create a new reference every render, which would trigger engine rebuild via\n // loadAudio deps. Content-based comparison avoids spurious rebuilds.\n const stableZoomLevels = useMemo(\n () => zoomLevels,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [zoomLevels?.join(',')]\n );\n\n // Annotations are derived from prop (single source of truth in parent)\n // In v6, annotations must be pre-parsed (numeric start/end). Use parseAeneas() from @waveform-playlist/annotations before passing.\n const annotations = useMemo(() => {\n if (!annotationList?.annotations) return [];\n if (process.env.NODE_ENV !== 'production' && annotationList.annotations.length > 0) {\n const first = annotationList.annotations[0] as unknown as Record<string, unknown>;\n if (typeof first.start !== 'number' || typeof first.end !== 'number') {\n console.error(\n '[waveform-playlist] Annotations must have numeric start/end values. ' +\n 'In v6, use parseAeneas() from @waveform-playlist/annotations before passing annotations. ' +\n 'Received start type: ' +\n typeof first.start\n );\n return [];\n }\n }\n return annotationList.annotations;\n }, [annotationList?.annotations]);\n\n // Ref for animation loop (avoids restarting loop on annotation change)\n const annotationsRef = useRef<AnnotationData[]>(annotations);\n annotationsRef.current = annotations;\n\n // State\n const [activeAnnotationId, setActiveAnnotationIdState] = useState<string | null>(null);\n const [isPlaying, setIsPlaying] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [audioBuffers, setAudioBuffers] = useState<AudioBuffer[]>([]);\n const [peaksDataArray, setPeaksDataArray] = useState<TrackClipPeaks[]>([]); // Updated for clip-based peaks\n const [trackStates, setTrackStates] = useState<TrackState[]>([]);\n const [isAutomaticScroll, setIsAutomaticScroll] = useState(automaticScroll);\n const [continuousPlay, setContinuousPlayState] = useState(\n annotationList?.isContinuousPlay ?? false\n );\n const [linkEndpoints, setLinkEndpoints] = useState(annotationList?.linkEndpoints ?? false);\n const [annotationsEditable, setAnnotationsEditable] = useState(annotationList?.editable ?? false);\n const [isReady, setIsReady] = useState(false);\n\n // Refs\n // Engine owns selection, loop, selectedTrackId, zoom, and masterVolume state.\n // React subscribes to engine statechange and mirrors into useState/refs.\n // Playback timing (currentTime, isPlaying) remains in React for animation loop.\n const engineRef = useRef<PlaylistEngine | null>(null);\n const adapterRef = useRef<PlayoutAdapter | null>(null);\n const audioInitializedRef = useRef<boolean>(false);\n const isPlayingRef = useRef<boolean>(false);\n isPlayingRef.current = isPlaying;\n const playStartPositionRef = useRef<number>(0);\n const currentTimeRef = useRef<number>(0);\n // Visually-aligned playback time (see toVisualTime / getAudibleTime). The\n // animation loop updates this per frame. Pause/seek/stop paths sync it\n // explicitly so the static playhead reflects the commanded resting position.\n const visualTimeRef = useRef<number>(0);\n const tracksRef = useRef<ClipTrack[]>(tracks);\n const soundFontCacheRef = useRef(soundFontCache);\n soundFontCacheRef.current = soundFontCache;\n const trackStatesRef = useRef<TrackState[]>(trackStates);\n const playbackStartTimeRef = useRef<number>(0); // context.currentTime when playback started\n const audioStartPositionRef = useRef<number>(0); // Audio position when playback started\n const playbackEndTimeRef = useRef<number | null>(null); // Audio position where playback should stop (for selections)\n const scrollContainerRef = useRef<HTMLDivElement | null>(null);\n const isAutomaticScrollRef = useRef<boolean>(false);\n // Animation frame callback registry — components register per-frame DOM update\n // callbacks instead of running their own rAF loops. Single loop drives all.\n const frameCallbacksRef = useRef<Map<string, (data: FrameData) => void>>(new Map());\n const continuousPlayRef = useRef<boolean>(annotationList?.isContinuousPlay ?? false);\n const activeAnnotationIdRef = useRef<string | null>(null);\n // Engine clip operations guard: tracks reference from the last engine statechange.\n // When engine methods (moveClip, trimClip, splitClip) mutate tracks, the statechange\n // handler stores state.tracks here and calls onTracksChange with the same reference.\n // loadAudio checks tracks === engineTracksRef.current to skip the full rebuild.\n const engineTracksRef = useRef<ClipTrack[] | null>(null);\n // Monotonic counter from engine.getState().tracksVersion — used to detect\n // track mutations (move/trim/split/add/remove) vs other statechange events.\n const lastTracksVersionRef = useRef(0);\n // Flag set during render to prevent the previous effect cleanup from disposing the\n // engine when we're skipping the rebuild. React runs previous cleanup before the\n // current effect body, so the flag must be set during render (which precedes cleanup).\n const skipEngineDisposeRef = useRef(false);\n // Set by useClipDragHandlers during boundary trim drags. When true,\n // loadAudio skips the full engine rebuild — visual updates flow via React\n // state only. On drag end, engine.trimClip() commits the final delta.\n const isDraggingRef = useRef(false);\n // Snapshot of tracks from the previous loadAudio run, used to detect\n // additive-only changes (new tracks appended, existing unchanged).\n const prevTracksRef = useRef<ClipTrack[]>([]);\n // Provider-level ref for scroll-position math and animation loop pixel\n // calculation. Distinct from useZoomControls's internal ref (statechange guard).\n const samplesPerPixelRef = useRef<number>(initialSamplesPerPixel);\n // Provisional sample rate until the adapter (which owns the AudioContext) is\n // created in loadAudio. Reconciled from adapter.audioContext.sampleRate there.\n // (#510: avoids a static @waveform-playlist/playout import at mount.)\n const [initialSampleRate] = useState<number>(() => sampleRateProp ?? 48000);\n const sampleRateRef = useRef<number>(initialSampleRate);\n\n // Custom hooks — engine-owned state delegated to hooks with onEngineState() pattern\n const { timeFormat, setTimeFormat, formatTime } = useTimeFormat();\n const zoom = useZoomControls({ engineRef, initialSamplesPerPixel });\n const { samplesPerPixel, onEngineState: onZoomEngineState } = zoom;\n const volume = useMasterVolume({ engineRef, initialVolume: 1.0 });\n const {\n masterVolume,\n setMasterVolume,\n masterVolumeRef,\n onEngineState: onVolumeEngineState,\n } = volume;\n const {\n selectionStart,\n selectionEnd,\n setSelection: setSelectionEngine,\n selectionStartRef,\n selectionEndRef,\n onEngineState: onSelectionEngineState,\n } = useSelectionState({ engineRef });\n const {\n isLoopEnabled,\n loopStart,\n loopEnd,\n setLoopEnabled,\n setLoopRegion,\n clearLoopRegion,\n isLoopEnabledRef,\n loopStartRef,\n loopEndRef,\n onEngineState: onLoopEngineState,\n } = useLoopState({ engineRef });\n const {\n selectedTrackId,\n setSelectedTrackId: setSelectedTrackIdControl,\n onEngineState: onSelectedTrackEngineState,\n selectedTrackIdRef,\n } = useSelectedTrack({ engineRef });\n const {\n canUndo,\n canRedo,\n undo,\n redo,\n onEngineState: onUndoEngineState,\n } = useUndoState({ engineRef });\n const { animationFrameRef, startAnimationFrameLoop, stopAnimationFrameLoop } =\n useAnimationFrameLoop();\n\n // Worker-based WaveformData cache for fast zoom resampling\n const baseScale = useMemo(\n () => Math.min(...(stableZoomLevels ?? [256, 512, 1024, 2048, 4096, 8192])),\n [stableZoomLevels]\n );\n const { cache: waveformDataCache } = useWaveformDataCache(tracks, baseScale);\n\n // Custom setter for continuousPlay that updates BOTH state and ref synchronously\n // This ensures the ref is updated immediately, before the animation loop can read it\n const setContinuousPlay = useCallback((value: boolean) => {\n continuousPlayRef.current = value; // Update ref synchronously\n setContinuousPlayState(value); // Update state (triggers re-render)\n }, []);\n\n // Custom setter for activeAnnotationId that updates BOTH state and ref synchronously\n const setActiveAnnotationId = useCallback((value: string | null) => {\n activeAnnotationIdRef.current = value; // Update ref synchronously\n setActiveAnnotationIdState(value); // Update state (triggers re-render)\n }, []);\n\n // Cross-hook concern: reads selection refs to set loop region\n const setLoopRegionFromSelection = useCallback(() => {\n const start = selectionStartRef.current ?? 0;\n const end = selectionEndRef.current ?? 0;\n if (start !== end && end > start) {\n setLoopRegion(start, end);\n }\n }, [setLoopRegion, selectionStartRef, selectionEndRef]);\n\n // Keep refs in sync with state\n useEffect(() => {\n isAutomaticScrollRef.current = isAutomaticScroll;\n }, [isAutomaticScroll]);\n\n useEffect(() => {\n trackStatesRef.current = trackStates;\n }, [trackStates]);\n\n tracksRef.current = tracks;\n\n // Render-phase guard: detect when tracks prop came from an engine clip operation.\n // Must be set before effects run so the previous effect cleanup can skip disposal.\n // Also skip disposal during active boundary trim drags — onDragMove updates React\n // tracks per-frame, triggering effect re-runs whose cleanup would dispose the engine.\n const isEngineTracks = tracks === engineTracksRef.current;\n\n // Detect additive-only track changes: new tracks appended, existing tracks unchanged.\n // Uses reference equality on individual tracks — if any existing track object changed\n // (e.g., clip added to it), this is false and triggers a full rebuild.\n const prevTracks = prevTracksRef.current;\n const isIncrementalAdd =\n engineRef.current !== null &&\n prevTracks.length > 0 &&\n tracks.length > prevTracks.length &&\n prevTracks.every((pt) => {\n const current = tracks.find((t) => t.id === pt.id);\n return current === pt; // reference equality — existing tracks must be untouched\n });\n\n skipEngineDisposeRef.current = isEngineTracks || isDraggingRef.current || isIncrementalAdd;\n\n // Adjust scroll position proportionally when zoom changes\n useEffect(() => {\n if (!scrollContainerRef.current || duration === 0) return;\n\n const container = scrollContainerRef.current;\n const oldSamplesPerPixel = samplesPerPixelRef.current;\n const newSamplesPerPixel = samplesPerPixel;\n\n if (oldSamplesPerPixel === newSamplesPerPixel) return;\n\n // Calculate the current center time in the viewport\n const containerWidth = container.clientWidth;\n const currentScrollLeft = container.scrollLeft;\n const centerPixel = currentScrollLeft + containerWidth / 2;\n const sr = sampleRateRef.current;\n const centerTime = (centerPixel * oldSamplesPerPixel) / sr;\n\n // Calculate new scroll position to keep the same center time\n const newCenterPixel = (centerTime * sr) / newSamplesPerPixel;\n const newScrollLeft = Math.max(0, newCenterPixel - containerWidth / 2);\n\n container.scrollLeft = newScrollLeft;\n samplesPerPixelRef.current = newSamplesPerPixel;\n }, [samplesPerPixel, duration]);\n\n // Track pending playback resume after tracks change\n const pendingResumeRef = useRef<{ position: number } | null>(null);\n\n // Load audio from clips (only when tracks change)\n useEffect(() => {\n // Guard: skip full rebuild when tracks came from an engine clip operation\n // (moveClip, trimClip, splitClip). The engine and adapter already have the\n // correct tracks — we just need to update duration for the timeline.\n // Also skip during active boundary trim drags — visual updates flow via\n // React state only; engine.trimClip() commits the final delta on drag end.\n if (isEngineTracks || isDraggingRef.current) {\n if (isEngineTracks) {\n engineTracksRef.current = null;\n }\n\n // Recalculate duration from updated clip positions\n let maxDuration = 0;\n tracks.forEach((track) => {\n track.clips.forEach((clip) => {\n const clipEnd = (clip.startSample + clip.durationSamples) / clip.sampleRate;\n maxDuration = Math.max(maxDuration, clipEnd);\n });\n });\n setDuration(maxDuration);\n prevTracksRef.current = tracks;\n return;\n }\n\n // Guard: incremental track addition — only new tracks appended, existing\n // tracks unchanged. Add each new track to the existing engine without\n // tearing down the playout. This avoids disposing and recreating all\n // audio nodes when dropping a file or finishing a recording.\n if (isIncrementalAdd && engineRef.current) {\n try {\n // Use prevTracks captured during render (not prevTracksRef.current) to ensure\n // the guard condition and inner logic operate on the same snapshot.\n const prevIds = new Set(prevTracks.map((t) => t.id));\n const addedTracks = tracks.filter((t) => !prevIds.has(t.id));\n\n // Merge current UI state into new tracks before adding to engine\n const currentTrackStates = trackStatesRef.current;\n for (const track of addedTracks) {\n const trackIndex = tracks.indexOf(track);\n const trackState = currentTrackStates[trackIndex];\n const trackWithState = {\n ...track,\n volume: trackState?.volume ?? track.volume,\n muted: trackState?.muted ?? track.muted,\n soloed: trackState?.soloed ?? track.soloed,\n pan: trackState?.pan ?? track.pan,\n };\n engineRef.current!.addTrack(trackWithState);\n }\n\n // Update duration from all tracks (including newly added)\n let maxDuration = 0;\n tracks.forEach((track) => {\n track.clips.forEach((clip) => {\n const clipEnd = (clip.startSample + clip.durationSamples) / clip.sampleRate;\n maxDuration = Math.max(maxDuration, clipEnd);\n });\n });\n setDuration(maxDuration);\n\n // Initialize track states for the new tracks (preserve existing)\n setTrackStates((prev) => {\n if (prev.length === tracks.length) return prev;\n const newStates = [...prev];\n for (const track of addedTracks) {\n newStates.push({\n name: track.name,\n muted: track.muted,\n soloed: track.soloed,\n volume: track.volume,\n pan: track.pan,\n });\n }\n return newStates;\n });\n\n // Extract audio buffers from all clips across all tracks\n const buffers: AudioBuffer[] = [];\n tracks.forEach((track) => {\n track.clips.forEach((clip) => {\n if (clip.audioBuffer) {\n buffers.push(clip.audioBuffer);\n }\n });\n });\n setAudioBuffers(buffers);\n\n prevTracksRef.current = tracks;\n return;\n } catch (err) {\n console.warn(\n '[waveform-playlist] Incremental add failed, falling through to full rebuild:',\n String(err)\n );\n // Fall through to full loadAudio path\n }\n }\n\n // Defer engine rebuild during progressive loading — tracks render visually\n // (controls, clip shapes, peaks via worker) but the engine isn't built yet.\n // When deferEngineRebuild flips to false, this effect re-runs and builds once.\n if (deferEngineRebuild) {\n // Still update duration so the timeline renders at the correct width\n let maxDuration = 0;\n tracks.forEach((track) => {\n track.clips.forEach((clip) => {\n const clipEnd = (clip.startSample + clip.durationSamples) / clip.sampleRate;\n maxDuration = Math.max(maxDuration, clipEnd);\n });\n });\n setDuration(maxDuration);\n return;\n }\n\n // Reset ready state for full rebuild\n setIsReady(false);\n\n if (tracks.length === 0) {\n // Clear state when all tracks are removed\n setAudioBuffers([]);\n setDuration(0);\n setTrackStates([]);\n setPeaksDataArray([]);\n if (engineRef.current) {\n engineRef.current.dispose();\n engineRef.current = null;\n adapterRef.current = null;\n }\n prevTracksRef.current = tracks;\n return;\n }\n\n // Capture playback state before rebuilding playout (read from ref, not state,\n // so isPlaying can be excluded from the dep array — play/pause must not trigger rebuilds)\n const wasPlaying = isPlayingRef.current;\n const resumePosition = currentTimeRef.current;\n\n // Stop current playback and animation before disposing\n if (engineRef.current && wasPlaying) {\n engineRef.current.stop();\n stopAnimationFrameLoop();\n // Mark that we need to resume playback after playout is rebuilt\n pendingResumeRef.current = { position: resumePosition };\n }\n\n let cancelled = false;\n\n const loadAudio = async () => {\n try {\n // Extract all audio buffers from all clips across all tracks\n const buffers: AudioBuffer[] = [];\n\n tracks.forEach((track) => {\n track.clips.forEach((clip) => {\n if (clip.audioBuffer) {\n buffers.push(clip.audioBuffer);\n }\n });\n });\n\n // Calculate total timeline duration from all clips across all tracks\n // Use clip.sampleRate which is always defined (works for peaks-only clips too)\n let maxDuration = 0;\n tracks.forEach((track) => {\n track.clips.forEach((clip) => {\n const sampleRate = clip.sampleRate;\n const clipEndSample = clip.startSample + clip.durationSamples;\n const clipEnd = clipEndSample / sampleRate;\n maxDuration = Math.max(maxDuration, clipEnd);\n });\n });\n\n setAudioBuffers(buffers);\n setDuration(maxDuration);\n\n // Initialize or update track states, preserving existing UI state (mute/solo/volume/pan)\n // Only initialize from ClipTrack props when trackStates is empty or track count changes\n setTrackStates((prevStates) => {\n if (prevStates.length === tracks.length) {\n // Same number of tracks - preserve existing UI state, just update names\n return prevStates.map((state, i) => ({\n ...state,\n name: tracks[i].name,\n }));\n }\n // Track count changed - reinitialize from ClipTrack properties\n return tracks.map((track) => ({\n name: track.name,\n muted: track.muted,\n soloed: track.soloed,\n volume: track.volume,\n pan: track.pan,\n }));\n });\n\n // Dispose old engine before creating new one\n if (engineRef.current) {\n engineRef.current.dispose();\n }\n\n // Reset tracks version tracking for the new engine\n lastTracksVersionRef.current = 0;\n engineTracksRef.current = null;\n\n // Create engine adapter — custom if provided, else Tone.js (dynamically imported).\n // Reset init flag — new adapter needs AudioContext resume on first play.\n audioInitializedRef.current = false;\n const adapter = await resolvePlayoutAdapter({\n createAdapter,\n effects,\n soundFontCache: soundFontCacheRef.current,\n sampleRate: sampleRateProp,\n });\n if (cancelled) {\n adapter.dispose();\n return;\n }\n // Reconcile sample rate from the adapter's own AudioContext. The setState\n // calls later in loadAudio trigger a re-render that propagates this via\n // `const sampleRate = sampleRateRef.current` at render.\n sampleRateRef.current = adapter.audioContext.sampleRate;\n adapterRef.current = adapter;\n const engine = new PlaylistEngine({\n adapter,\n samplesPerPixel: samplesPerPixelRef.current,\n zoomLevels: stableZoomLevels,\n });\n\n // Seed engine with current UI state so a fresh engine doesn't\n // reset selection/loop/selectedTrack to defaults on the first statechange emission.\n engine.setSelection(selectionStartRef.current ?? 0, selectionEndRef.current ?? 0);\n engine.setLoopRegion(loopStartRef.current ?? 0, loopEndRef.current ?? 0);\n if (isLoopEnabledRef.current) engine.setLoopEnabled(true);\n engine.setMasterVolume(masterVolumeRef.current ?? 1.0);\n if (selectedTrackIdRef.current) engine.selectTrack(selectedTrackIdRef.current);\n\n // Merge current UI state into tracks before passing to engine\n const currentTrackStates = trackStatesRef.current;\n const tracksWithState = tracks.map((track, index) => {\n const trackState = currentTrackStates[index];\n return {\n ...track,\n volume: trackState?.volume ?? track.volume,\n muted: trackState?.muted ?? track.muted,\n soloed: trackState?.soloed ?? track.soloed,\n pan: trackState?.pan ?? track.pan,\n };\n });\n\n // Subscribe to engine statechange BEFORE setTracks() so the first\n // emission (which carries correct canZoomIn/canZoomOut) is captured.\n // Seeding statechanges fired above are harmless — they contain values\n // that already match the hooks' current state, so onEngineState()\n // ref guards skip them.\n // Suppress tracks mirroring during the initial setTracks — the parent\n // already has these tracks. The flag is local so it becomes false\n // after setTracks returns (statechange fires synchronously).\n let suppressTracksMirroring = true;\n engine.on('statechange', (state: EngineState) => {\n onSelectionEngineState(state);\n onLoopEngineState(state);\n onSelectedTrackEngineState(state);\n onZoomEngineState(state);\n onVolumeEngineState(state);\n onUndoEngineState(state);\n\n // Mirror engine tracks changes to parent via onTracksChange.\n // tracksVersion only increments on track mutations (move, trim, split,\n // setTracks, addTrack, removeTrack), not on selection/zoom/volume changes.\n if (!suppressTracksMirroring && state.tracksVersion !== lastTracksVersionRef.current) {\n lastTracksVersionRef.current = state.tracksVersion;\n engineTracksRef.current = state.tracks;\n if (onTracksChange) {\n onTracksChange(state.tracks);\n } else {\n console.warn(\n '[waveform-playlist] Engine tracks changed but onTracksChange prop is not set — ' +\n 'UI will revert on next render. Pass onTracksChange to WaveformPlaylistProvider.'\n );\n }\n }\n });\n\n engine.setTracks(tracksWithState);\n suppressTracksMirroring = false;\n lastTracksVersionRef.current = engine.getState().tracksVersion;\n engineRef.current = engine;\n\n setIsReady(true);\n\n // Dispatch custom event for external listeners\n const event = new CustomEvent('waveform-playlist:ready', {\n detail: {\n trackCount: tracks.length,\n duration: maxDuration,\n },\n });\n window.dispatchEvent(event);\n\n prevTracksRef.current = tracks;\n\n onReady?.();\n } catch (error) {\n // Dispose an adapter created before the engine failed to build (e.g. invalid\n // zoomLevels make `new PlaylistEngine` throw) — the cleanup skips it because\n // engineRef was never assigned, so it would otherwise leak a live AudioContext.\n if (!engineRef.current && adapterRef.current) {\n try {\n adapterRef.current.dispose();\n } catch (disposeErr) {\n console.warn(\n '[waveform-playlist] adapter dispose after load failure threw: ' + String(disposeErr)\n );\n }\n adapterRef.current = null;\n }\n console.warn('[waveform-playlist] Error loading audio: ' + String(error));\n onError?.(error instanceof Error ? error : new Error(String(error)));\n }\n };\n\n loadAudio();\n\n return () => {\n cancelled = true;\n // Skip disposal when the next render's guard will keep the engine alive.\n // skipEngineDisposeRef is set during the render phase (before this cleanup runs).\n if (skipEngineDisposeRef.current) {\n skipEngineDisposeRef.current = false;\n return;\n }\n stopAnimationFrameLoop();\n if (engineRef.current) {\n engineRef.current.dispose();\n adapterRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n tracks,\n // isEngineTracks is intentionally excluded — it's a render-phase guard read\n // inside the effect body, not a trigger. Including it causes a spurious re-run\n // when it flips from true→false after engineTracksRef is cleared.\n // isPlaying is intentionally excluded — read from isPlayingRef inside the\n // effect body. Including it causes a full engine+playout rebuild on every\n // play/pause/stop, destroying and recreating all audio Players.\n // soundFontCache is deliberately excluded — late cache changes are forwarded\n // to the live adapter by the sync effect below; only adapter creation reads it\n // (via soundFontCacheRef).\n onReady,\n onError,\n effects,\n stopAnimationFrameLoop,\n onSelectionEngineState,\n onLoopEngineState,\n onSelectedTrackEngineState,\n onZoomEngineState,\n onVolumeEngineState,\n onUndoEngineState,\n onTracksChange,\n masterVolumeRef,\n selectionStartRef,\n selectionEndRef,\n loopStartRef,\n loopEndRef,\n isLoopEnabledRef,\n stableZoomLevels,\n deferEngineRebuild,\n ]);\n\n // Forward late-arriving / swapped SoundFontCache to the live adapter so\n // MIDI tracks upgrade from PolySynth without an engine rebuild. On mount\n // this may run against a just-created adapter (the loadAudio effect is\n // declared earlier, runs first, and assigns adapterRef synchronously) or\n // against null (empty tracks). Both are safe: the helper no-ops on null,\n // and the adapter skips MIDI tracks whose routing is unchanged.\n useEffect(() => {\n syncSoundFontCacheToAdapter(adapterRef.current, soundFontCache);\n }, [soundFontCache]);\n\n // Regenerate peaks when zoom, mono, or waveformDataCache changes (without reloading audio)\n // Peak sources in priority order:\n // A) clip.waveformData — external pre-computed peaks (e.g. from BBC audiowaveform)\n // B) waveformDataCache — worker-generated WaveformData (fast resample on zoom)\n // C) empty peaks — clip is still loading or has no audio data\n useEffect(() => {\n if (tracks.length === 0) return;\n\n const allTrackPeaks: TrackClipPeaks[] = tracks.map((track) => {\n const clipPeaks: ClipPeaks[] = track.clips.map((clip) => {\n let peaks: PeakData | undefined;\n\n // Path A: External pre-computed waveform data (e.g. from audiowaveform .dat file)\n // When sample rates match, use clip offsets directly.\n // When rates mismatch (e.g., waveformData at 48000 Hz, clip rebuilt at 44100 Hz\n // after audio decode), convert offsets to the waveformData's sample space and\n // adjust samplesPerPixel so peaks fill the correct container width.\n // Path B (worker) replaces these on next render once cache is populated.\n if (clip.waveformData) {\n try {\n const wdRate = clip.waveformData.sample_rate;\n const clipRate = clip.sampleRate;\n let peakOffset = clip.offsetSamples;\n let peakDuration = clip.durationSamples;\n let peakSpp = samplesPerPixel;\n if (wdRate !== clipRate && clipRate > 0 && wdRate > 0) {\n const ratio = wdRate / clipRate;\n peakOffset = Math.round(clip.offsetSamples * ratio);\n peakDuration = Math.round(clip.durationSamples * ratio);\n peakSpp = Math.max(1, Math.round(samplesPerPixel * ratio));\n }\n peaks = extractPeaksFromWaveformDataFull(\n clip.waveformData as WaveformData,\n peakSpp,\n mono,\n peakOffset,\n peakDuration\n );\n } catch (err) {\n console.warn(\n '[waveform-playlist] Failed to extract peaks from waveformData: ' + String(err)\n );\n }\n }\n\n // Path B: Worker-generated WaveformData cache (fast resample on zoom)\n if (!peaks) {\n const cached = waveformDataCache.get(clip.id);\n if (cached) {\n try {\n peaks = extractPeaksFromWaveformDataFull(\n cached,\n samplesPerPixel,\n mono,\n clip.offsetSamples,\n clip.durationSamples\n );\n } catch (err) {\n console.warn(\n '[waveform-playlist] Failed to extract peaks from cache: ' + String(err)\n );\n }\n }\n }\n\n // Path C: No peaks data available yet — render empty while worker processes.\n // Use correct channel count from audioBuffer to prevent track height shift\n // when peaks arrive (mono mode collapses to 1 channel).\n if (!peaks) {\n if (!deferEngineRebuild && !clip.audioBuffer && !clip.waveformData && !clip.midiNotes) {\n console.warn(\n `[waveform-playlist] Clip \"${clip.id}\" has no audio data or waveform data`\n );\n }\n const numChannels = mono ? 1 : (clip.audioBuffer?.numberOfChannels ?? 1);\n // MIDI clips: derive pixel width from sample duration so the clip renders\n // at the correct width instead of zero-width blank tracks.\n const pixelLength = clip.midiNotes\n ? Math.ceil(clip.durationSamples / samplesPerPixel)\n : 0;\n peaks = {\n length: pixelLength,\n data: Array.from({ length: numChannels }, () => new Int16Array(pixelLength * 2)),\n bits: 16,\n };\n }\n\n return {\n clipId: clip.id,\n trackName: track.name,\n peaks,\n startSample: clip.startSample,\n durationSamples: clip.durationSamples,\n fadeIn: clip.fadeIn,\n fadeOut: clip.fadeOut,\n midiNotes: clip.midiNotes,\n sampleRate: clip.sampleRate,\n offsetSamples: clip.offsetSamples,\n };\n });\n\n return clipPeaks;\n });\n\n setPeaksDataArray(allTrackPeaks);\n }, [tracks, samplesPerPixel, mono, waveformDataCache, deferEngineRebuild]);\n\n // Audio clock from the adapter's own AudioContext (#510 — replaces tone's\n // getContext()). Stable; reads a ref so identity never changes.\n const getAudioContextTime = useCallback(\n () => adapterRef.current?.audioContext.currentTime ?? 0,\n []\n );\n\n // Returns current playback time from engine (auto-wraps at loop boundaries).\n // Falls back to manual calculation when engine is unavailable.\n const getPlaybackTimeFallbackWarnedRef = useRef(false);\n const getPlaybackTime = useCallback(() => {\n if (engineRef.current) {\n return engineRef.current.getCurrentTime();\n }\n // Fallback: manual calculation (does not handle loop wrapping)\n if (!getPlaybackTimeFallbackWarnedRef.current) {\n getPlaybackTimeFallbackWarnedRef.current = true;\n console.warn(\n '[waveform-playlist] getPlaybackTime called without engine. ' +\n 'Falling back to manual elapsed time (loop wrapping will not work).'\n );\n }\n const elapsed = getAudioContextTime() - (playbackStartTimeRef.current ?? 0);\n return (audioStartPositionRef.current ?? 0) + elapsed;\n }, [getAudioContextTime]);\n\n // Resting (non-playing) cursor positions display the raw time unchanged.\n // Latency compensation (outputLatency + lookAhead) models the gap between\n // the Transport's scheduling position and audible output — a property of\n // *playing*, not of position. A seeked/paused/stopped cursor has no audible\n // counterpart; the commanded position is the truth. During playback the\n // animation loop uses engine.getAudibleTime() instead.\n const toVisualTime = useCallback((rawTime: number): number => {\n return Number.isFinite(rawTime) ? Math.max(0, rawTime) : 0;\n }, []);\n\n // Pair-write helper for the dual-ref pattern. Storage stays raw on\n // `currentTimeRef`; display gets the guard-clamped raw version on `visualTimeRef`\n // (compensation happens in the animation loop via engine.getAudibleTime() while playing).\n // Use this at every pause/seek/stop/loop site — never assign one ref alone\n // (browser/CLAUDE.md: \"Display uses visualTimeRef\").\n const setCurrentTimeRefs = useCallback(\n (rawTime: number) => {\n currentTimeRef.current = rawTime;\n visualTimeRef.current = toVisualTime(rawTime);\n },\n [toVisualTime]\n );\n\n // Read the engine's scheduler lookahead (Tone adapter ~0.1s, native 0).\n // Exposed through PlaybackAnimationContextValue so consumers (e.g., recording\n // live preview) compute audible-latency from the same source as the playhead.\n const getLookAhead = useCallback((): number => {\n return engineRef.current?.lookAhead ?? 0;\n }, []);\n\n const getOutputLatency = useCallback((): number => {\n const audioCtx = adapterRef.current?.audioContext;\n return audioCtx && 'outputLatency' in audioCtx ? (audioCtx as AudioContext).outputLatency : 0;\n }, []);\n\n const registerFrameCallback = useCallback((id: string, cb: (data: FrameData) => void) => {\n frameCallbacksRef.current.set(id, cb);\n }, []);\n\n const unregisterFrameCallback = useCallback((id: string) => {\n frameCallbacksRef.current.delete(id);\n }, []);\n\n // Animation loop\n const startAnimationLoop = useCallback(() => {\n const updateTime = () => {\n // Get current time from engine (auto-wraps at loop boundaries via Transport.seconds)\n const time = getPlaybackTime();\n currentTimeRef.current = time;\n\n // Compute visual time once — all visual consumers use this same value.\n // engine.getAudibleTime() subtracts outputLatency (hardware DAC delay)\n // and adapter.lookAhead (Tone.js Transport runs ~100ms ahead of audible),\n // holding at the play-start position during the pre-roll window. Native\n // adapters report lookAhead 0, so this is a near no-op there.\n const visualRaw = engineRef.current ? engineRef.current.getAudibleTime() : time;\n const visualTime = Number.isFinite(visualRaw) ? Math.max(0, visualRaw) : 0;\n visualTimeRef.current = visualTime;\n\n // Drive registered per-frame callbacks BEFORE stop checks so the\n // final frame renders at the correct stop position (not one frame behind).\n const sr = sampleRateRef.current;\n const spp = samplesPerPixelRef.current;\n const frameData: FrameData = {\n time,\n visualTime,\n sampleRate: sr,\n samplesPerPixel: spp,\n };\n for (const cb of frameCallbacksRef.current.values()) {\n cb(frameData);\n }\n\n // Handle annotation playback based on continuous play mode\n const currentAnnotations = annotationsRef.current;\n if (currentAnnotations.length > 0) {\n const currentAnnotation = currentAnnotations.find(\n (ann) => time >= ann.start && time < ann.end\n );\n\n if (continuousPlayRef.current) {\n // Continuous play ON: update active annotation, let audio play to the end\n if (currentAnnotation && currentAnnotation.id !== activeAnnotationIdRef.current) {\n setActiveAnnotationId(currentAnnotation.id);\n } else if (!currentAnnotation && activeAnnotationIdRef.current !== null) {\n // Clear the active annotation when we're past it, but don't stop playback\n // Let playback continue until the audio actually ends (handled by duration check)\n setActiveAnnotationId(null);\n }\n } else {\n // Continuous play OFF: stop at end of current annotation\n if (activeAnnotationIdRef.current) {\n const activeAnnotation = currentAnnotations.find(\n (ann) => ann.id === activeAnnotationIdRef.current\n );\n if (activeAnnotation && time >= activeAnnotation.end) {\n // Stop playback at end of current annotation\n if (engineRef.current) {\n engineRef.current.stop();\n }\n setIsPlaying(false);\n setCurrentTimeRefs(playStartPositionRef.current);\n setCurrentTime(playStartPositionRef.current);\n return;\n }\n } else {\n // If no active annotation ID is set, use the current annotation\n if (currentAnnotation) {\n setActiveAnnotationId(currentAnnotation.id);\n }\n }\n }\n }\n\n // Handle automatic scroll — uses shared visualTime for alignment with playhead\n if (isAutomaticScrollRef.current && scrollContainerRef.current && duration > 0) {\n const container = scrollContainerRef.current;\n const pixelPosition = (visualTime * sr) / spp;\n const containerWidth = container.clientWidth;\n\n // Continuously scroll to keep playhead centered.\n // Math.round prevents sub-pixel jitter from browser integer rounding.\n const targetScrollLeft = Math.round(Math.max(0, pixelPosition - containerWidth / 2));\n container.scrollLeft = targetScrollLeft;\n }\n\n // Check if we've reached the playback end time (for selection playback)\n if (playbackEndTimeRef.current !== null && time >= playbackEndTimeRef.current) {\n // Stop playback at selection end (selection playback is separate from looping)\n if (engineRef.current) {\n engineRef.current.stop();\n }\n setIsPlaying(false);\n setCurrentTimeRefs(playbackEndTimeRef.current);\n setCurrentTime(playbackEndTimeRef.current);\n playbackEndTimeRef.current = null; // Clear the end time\n return;\n }\n\n // Loop is handled natively by Tone.js Transport (Transport.loop/loopStart/loopEnd).\n // Transport.seconds auto-wraps at loop boundaries, so getPlaybackTime() returns\n // the correct position without manual detection here.\n\n if (time >= duration && !indefinitePlaybackRef.current) {\n // Stop playback - inline to avoid circular dependency\n if (engineRef.current) {\n engineRef.current.stop();\n }\n setIsPlaying(false);\n setCurrentTimeRefs(playStartPositionRef.current);\n setCurrentTime(playStartPositionRef.current);\n setActiveAnnotationId(null);\n return;\n }\n startAnimationFrameLoop(updateTime);\n };\n startAnimationFrameLoop(updateTime);\n }, [\n duration,\n setActiveAnnotationId,\n startAnimationFrameLoop,\n getPlaybackTime,\n setCurrentTimeRefs,\n ]);\n\n const stopAnimationLoop = stopAnimationFrameLoop;\n\n // Restart animation loop and reschedule playout when continuousPlay changes during playback\n // This ensures the loop always has the current continuousPlay value\n // and removes duration limits when switching to continuous play\n useEffect(() => {\n const reschedulePlayback = async () => {\n if (isPlaying && animationFrameRef.current && engineRef.current) {\n // When toggling continuous play ON, reschedule playout without duration limit\n // so audio continues past the current annotation boundary\n if (continuousPlay) {\n const currentPos = currentTimeRef.current;\n\n // Stop current playout (which may have duration limit + pause callback)\n engineRef.current.stop();\n stopAnimationLoop();\n\n const timeNow = getAudioContextTime();\n playbackStartTimeRef.current = timeNow;\n audioStartPositionRef.current = currentPos;\n\n // Play without duration - will play to end of track\n engineRef.current.play(currentPos);\n startAnimationLoop();\n } else {\n // Just restart animation loop for continuous play OFF\n stopAnimationLoop();\n startAnimationLoop();\n }\n }\n };\n\n reschedulePlayback().catch((err) => {\n console.warn('[waveform-playlist] Failed to reschedule playback:', err);\n setIsPlaying(false);\n stopAnimationLoop();\n });\n }, [\n continuousPlay,\n isPlaying,\n startAnimationLoop,\n stopAnimationLoop,\n animationFrameRef,\n getAudioContextTime,\n ]);\n\n // Resume playback after tracks change (e.g., after splitting a clip during playback)\n useEffect(() => {\n const resumePlayback = async () => {\n if (pendingResumeRef.current && engineRef.current) {\n const { position } = pendingResumeRef.current;\n pendingResumeRef.current = null;\n\n const timeNow = getAudioContextTime();\n playbackStartTimeRef.current = timeNow;\n audioStartPositionRef.current = position;\n\n if (!audioInitializedRef.current) {\n await engineRef.current.init();\n audioInitializedRef.current = true;\n }\n\n engineRef.current.play(position);\n setIsPlaying(true);\n startAnimationLoop();\n }\n };\n\n resumePlayback().catch((err) => {\n console.warn('[waveform-playlist] Failed to resume playback after track change:', err);\n setIsPlaying(false);\n stopAnimationLoop();\n });\n }, [tracks, startAnimationLoop, stopAnimationLoop, getAudioContextTime]);\n\n // Playback controls\n const play = useCallback(\n async (startTime?: number, playDuration?: number) => {\n if (!engineRef.current) return;\n\n const actualStartTime = startTime ?? currentTimeRef.current;\n playStartPositionRef.current = actualStartTime;\n\n // Update currentTimeRef to match the actual start position\n // This ensures the animation loop starts from the correct position\n setCurrentTimeRefs(actualStartTime);\n\n // Stop existing playback and animation loop before restarting.\n // engine.stop() resets engine._currentTime to _playStartPosition (could be 0)\n // and sets _isPlaying=false. AnimatedPlayhead's RAF may fire during this\n // window and call engine.getCurrentTime() which would return 0.\n // The seek() immediately sets _currentTime to the intended start position,\n // preventing the cursor flash.\n engineRef.current.stop();\n engineRef.current.seek(actualStartTime);\n stopAnimationLoop();\n\n // Record timing for accurate position tracking using the adapter's AudioContext\n const startTimeNow = getAudioContextTime();\n playbackStartTimeRef.current = startTimeNow;\n audioStartPositionRef.current = actualStartTime;\n\n // Set playback end time if playing with duration (e.g., selection playback)\n playbackEndTimeRef.current =\n playDuration !== undefined ? actualStartTime + playDuration : null;\n\n // Don't set up playback complete callback for annotations\n // The animation loop handles stopping at annotation boundaries\n // This avoids callback timing issues when switching between annotations\n\n // Ensure AudioContext is resumed (requires user gesture, no-op after first call).\n // Guarded by ref so subsequent plays skip the await entirely.\n if (!audioInitializedRef.current) {\n await engineRef.current.init();\n audioInitializedRef.current = true;\n }\n\n const endTime = playDuration !== undefined ? actualStartTime + playDuration : undefined;\n try {\n engineRef.current.play(actualStartTime, endTime);\n } catch (err) {\n console.warn('[waveform-playlist] Playback failed to start:', err);\n stopAnimationLoop();\n return;\n }\n setIsPlaying(true);\n startAnimationLoop();\n },\n [startAnimationLoop, stopAnimationLoop, setCurrentTimeRefs, getAudioContextTime]\n );\n\n const pause = useCallback(() => {\n if (!engineRef.current) return;\n\n // Get current position from engine (auto-wraps at loop boundaries)\n const pauseTime = getPlaybackTime();\n\n engineRef.current.pause();\n setIsPlaying(false);\n stopAnimationLoop();\n\n // Update to the calculated pause position\n setCurrentTimeRefs(pauseTime);\n setCurrentTime(pauseTime);\n }, [stopAnimationLoop, getPlaybackTime, setCurrentTimeRefs]);\n\n const stop = useCallback(() => {\n if (!engineRef.current) return;\n\n engineRef.current.stop();\n setIsPlaying(false);\n stopAnimationLoop();\n\n // Return cursor to where playback started (Audacity-style)\n setCurrentTimeRefs(playStartPositionRef.current);\n setCurrentTime(playStartPositionRef.current);\n setActiveAnnotationId(null);\n }, [stopAnimationLoop, setActiveAnnotationId, setCurrentTimeRefs]);\n\n // Seek to a specific time - works whether playing or stopped\n const seekTo = useCallback(\n (time: number) => {\n // Clamp time to valid range\n const clampedTime = Math.max(0, Math.min(time, duration));\n\n // Update the current time state\n setCurrentTimeRefs(clampedTime);\n setCurrentTime(clampedTime);\n\n // If currently playing, restart at the new position.\n // play() handles stop+seek+restart internally.\n if (isPlaying && engineRef.current) {\n play(clampedTime);\n }\n },\n [duration, isPlaying, play, setCurrentTimeRefs]\n );\n\n // Track controls\n const setTrackMute = useCallback(\n (trackIndex: number, muted: boolean) => {\n const trackId = tracksRef.current[trackIndex]?.id;\n if (!trackId) return;\n\n const newStates = [...trackStates];\n newStates[trackIndex] = { ...newStates[trackIndex], muted };\n setTrackStates(newStates);\n\n if (engineRef.current) {\n engineRef.current.setTrackMute(trackId, muted);\n }\n },\n [trackStates]\n );\n\n const setTrackSolo = useCallback(\n (trackIndex: number, soloed: boolean) => {\n const trackId = tracksRef.current[trackIndex]?.id;\n if (!trackId) return;\n\n const newStates = [...trackStates];\n newStates[trackIndex] = { ...newStates[trackIndex], soloed };\n setTrackStates(newStates);\n\n if (engineRef.current) {\n engineRef.current.setTrackSolo(trackId, soloed);\n }\n },\n [trackStates]\n );\n\n const setTrackVolume = useCallback(\n (trackIndex: number, volume: number) => {\n const trackId = tracksRef.current[trackIndex]?.id;\n if (!trackId) return;\n\n const newStates = [...trackStates];\n newStates[trackIndex] = { ...newStates[trackIndex], volume };\n setTrackStates(newStates);\n\n if (engineRef.current) {\n engineRef.current.setTrackVolume(trackId, volume);\n }\n },\n [trackStates]\n );\n\n const setTrackPan = useCallback(\n (trackIndex: number, pan: number) => {\n const trackId = tracksRef.current[trackIndex]?.id;\n if (!trackId) return;\n\n const newStates = [...trackStates];\n newStates[trackIndex] = { ...newStates[trackIndex], pan };\n setTrackStates(newStates);\n\n if (engineRef.current) {\n engineRef.current.setTrackPan(trackId, pan);\n }\n },\n [trackStates]\n );\n\n // Selection — wraps hook setter with playback side-effects (provider concern).\n const setSelection = useCallback(\n (start: number, end: number) => {\n setSelectionEngine(start, end);\n setCurrentTimeRefs(start);\n setCurrentTime(start);\n\n if (isPlaying && engineRef.current) {\n // Stop + seek before restarting playback from selection start.\n // engine.stop() resets _currentTime and _isPlaying, creating a window\n // where getCurrentTime() returns wrong value. seek() immediately\n // corrects _currentTime so any RAF firing in between reads the\n // intended position. engine.play() then restarts everything.\n engineRef.current.stop();\n engineRef.current.seek(start);\n engineRef.current.play(start);\n }\n },\n [isPlaying, setSelectionEngine, setCurrentTimeRefs]\n );\n\n // Memoize setScrollContainer callback\n const setScrollContainer = useCallback((element: HTMLDivElement | null) => {\n scrollContainerRef.current = element;\n }, []);\n\n // Stable callback ref for onAnnotationsChange to avoid re-creating controls context\n const onAnnotationsChangeRef = useRef(onAnnotationsChange);\n onAnnotationsChangeRef.current = onAnnotationsChange;\n\n const setAnnotations: React.Dispatch<React.SetStateAction<AnnotationData[]>> = useCallback(\n (action) => {\n const updated = typeof action === 'function' ? action(annotationsRef.current) : action;\n if (!onAnnotationsChangeRef.current) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n 'waveform-playlist: setAnnotations was called but no onAnnotationsChange callback is provided. ' +\n 'Annotation edits will not persist. Pass onAnnotationsChange to WaveformPlaylistProvider to handle annotation updates.'\n );\n }\n return;\n }\n onAnnotationsChangeRef.current(updated);\n },\n []\n );\n\n const sampleRate = sampleRateRef.current;\n const timeScaleHeight = timescale ? 30 : 0;\n const minimumPlaylistHeight = tracks.length * waveHeight + timeScaleHeight;\n\n // Split context values for performance optimization\n // Animation context only re-renders consumers on discrete events\n // (play/pause/stop/seek), never during the animation loop itself\n\n const animationValue: PlaybackAnimationContextValue = useMemo(\n () => ({\n isPlaying,\n currentTime,\n currentTimeRef,\n visualTimeRef,\n playbackStartTimeRef,\n audioStartPositionRef,\n getPlaybackTime,\n getAudioContextTime,\n getLookAhead,\n getOutputLatency,\n registerFrameCallback,\n unregisterFrameCallback,\n }),\n [\n isPlaying,\n currentTime,\n currentTimeRef,\n visualTimeRef,\n playbackStartTimeRef,\n audioStartPositionRef,\n getPlaybackTime,\n getAudioContextTime,\n getLookAhead,\n getOutputLatency,\n registerFrameCallback,\n unregisterFrameCallback,\n ]\n );\n\n const stateValue: PlaylistStateContextValue = useMemo(\n () => ({\n continuousPlay,\n linkEndpoints,\n annotationsEditable,\n isAutomaticScroll,\n isLoopEnabled,\n annotations,\n activeAnnotationId,\n selectionStart,\n selectionEnd,\n selectedTrackId,\n loopStart,\n loopEnd,\n indefinitePlayback,\n canUndo,\n canRedo,\n }),\n [\n continuousPlay,\n linkEndpoints,\n annotationsEditable,\n isAutomaticScroll,\n isLoopEnabled,\n annotations,\n activeAnnotationId,\n selectionStart,\n selectionEnd,\n selectedTrackId,\n loopStart,\n loopEnd,\n indefinitePlayback,\n canUndo,\n canRedo,\n ]\n );\n\n const setCurrentTimeControl = useCallback(\n (time: number) => {\n setCurrentTimeRefs(time);\n setCurrentTime(time);\n },\n [setCurrentTimeRefs]\n );\n\n const setAutomaticScrollControl = useCallback((enabled: boolean) => {\n setIsAutomaticScroll(enabled);\n }, []);\n\n const controlsValue: PlaylistControlsContextValue = useMemo(\n () => ({\n // Playback controls\n play,\n pause,\n stop,\n seekTo,\n setCurrentTime: setCurrentTimeControl,\n\n // Track controls\n setTrackMute,\n setTrackSolo,\n setTrackVolume,\n setTrackPan,\n\n // Selection\n setSelection,\n setSelectedTrackId: setSelectedTrackIdControl,\n\n // Time format\n setTimeFormat,\n formatTime,\n\n // Zoom\n zoomIn: zoom.zoomIn,\n zoomOut: zoom.zoomOut,\n\n // Master volume\n setMasterVolume,\n\n // Automatic scroll\n setAutomaticScroll: setAutomaticScrollControl,\n setScrollContainer,\n scrollContainerRef,\n\n // Annotation controls\n setContinuousPlay,\n setLinkEndpoints,\n setAnnotationsEditable,\n setAnnotations,\n setActiveAnnotationId,\n\n // Loop controls\n setLoopEnabled,\n setLoopRegion,\n setLoopRegionFromSelection,\n clearLoopRegion,\n\n // Undo/redo\n undo,\n redo,\n }),\n [\n play,\n pause,\n stop,\n seekTo,\n setCurrentTimeControl,\n setTrackMute,\n setTrackSolo,\n setTrackVolume,\n setTrackPan,\n setSelection,\n setSelectedTrackIdControl,\n setTimeFormat,\n formatTime,\n zoom.zoomIn,\n zoom.zoomOut,\n setMasterVolume,\n setAutomaticScrollControl,\n setScrollContainer,\n scrollContainerRef,\n setContinuousPlay,\n setLinkEndpoints,\n setAnnotationsEditable,\n setAnnotations,\n setActiveAnnotationId,\n setLoopEnabled,\n setLoopRegion,\n setLoopRegionFromSelection,\n clearLoopRegion,\n undo,\n redo,\n ]\n );\n\n const dataValue: PlaylistDataContextValue = useMemo(\n () => ({\n duration,\n audioBuffers,\n peaksDataArray,\n trackStates,\n tracks,\n sampleRate,\n waveHeight,\n timeScaleHeight,\n minimumPlaylistHeight,\n controls,\n playoutRef: engineRef,\n samplesPerPixel,\n timeFormat,\n masterVolume,\n canZoomIn: zoom.canZoomIn,\n canZoomOut: zoom.canZoomOut,\n barWidth,\n barGap,\n progressBarWidth,\n isReady,\n mono,\n isDraggingRef,\n onTracksChange,\n }),\n [\n duration,\n audioBuffers,\n peaksDataArray,\n trackStates,\n tracks,\n sampleRate,\n waveHeight,\n timeScaleHeight,\n minimumPlaylistHeight,\n controls,\n engineRef,\n samplesPerPixel,\n timeFormat,\n masterVolume,\n zoom.canZoomIn,\n zoom.canZoomOut,\n barWidth,\n barGap,\n progressBarWidth,\n isReady,\n mono,\n isDraggingRef,\n onTracksChange,\n ]\n );\n\n // Merge user theme with default theme\n const mergedTheme = { ...defaultTheme, ...userTheme };\n\n return (\n <ThemeProvider theme={mergedTheme}>\n <PlaybackAnimationContext.Provider value={animationValue}>\n <PlaylistStateContext.Provider value={stateValue}>\n <PlaylistControlsContext.Provider value={controlsValue}>\n <PlaylistDataContext.Provider value={dataValue}>\n {children}\n </PlaylistDataContext.Provider>\n </PlaylistControlsContext.Provider>\n </PlaylistStateContext.Provider>\n </PlaybackAnimationContext.Provider>\n </ThemeProvider>\n );\n};\n\n// Individual hooks for each context - use these for optimal performance\n// Components only re-render when their specific context data changes\n\nexport const usePlaybackAnimation = () => {\n const context = useContext(PlaybackAnimationContext);\n if (!context) {\n throw new Error('usePlaybackAnimation must be used within WaveformPlaylistProvider');\n }\n return context;\n};\n\nexport const usePlaylistState = () => {\n const context = useContext(PlaylistStateContext);\n if (!context) {\n throw new Error('usePlaylistState must be used within WaveformPlaylistProvider');\n }\n return context;\n};\n\nexport const usePlaylistControls = () => {\n const context = useContext(PlaylistControlsContext);\n if (!context) {\n throw new Error('usePlaylistControls must be used within WaveformPlaylistProvider');\n }\n return context;\n};\n\nexport const usePlaylistData = () => {\n const context = useContext(PlaylistDataContext);\n if (!context) {\n throw new Error('usePlaylistData must be used within WaveformPlaylistProvider');\n }\n return context;\n};\n\n/**\n * Like {@link usePlaylistData} but returns `null` instead of throwing when there\n * is no WaveformPlaylistProvider ancestor. Use this in hooks/components that may\n * also run in the MediaElement path (MediaElementPlaylistProvider), where the\n * WebAudio playlist context is absent.\n */\nexport const usePlaylistDataOptional = () => useContext(PlaylistDataContext);\n"],"mappings":"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","names":["import_react","import_react","import_tone","import_tone","import_react","import_react","import_core","Volume","import_react","import_core","import_playout","arrayBuffer","audioBuffer","import_react","import_playout","import_core","_a","import_ui_components","import_react","import_jsx_runtime"]}
|