@waveform-playlist/recording 12.2.0 → 13.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/README.md +153 -139
- package/dist/index.d.mts +35 -10
- package/dist/index.d.ts +35 -10
- package/dist/index.js +104 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +110 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer, appendPeaks } from '@waveform-playlist/core';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { addRecordingWorkletModule } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n // Resolved when the worklet posts the final done message after a stop command.\n const stopAckResolveRef = useRef<(() => void) | null>(null);\n // True from the start of stopRecording until it resolves. Distinct from\n // isPausedRef (user explicitly paused) — both freeze the duration tick,\n // but conflating them risks pauseRecording becoming a no-op while we're\n // stopping, or vice versa.\n const isStoppingRef = useRef<boolean>(false);\n\n // Shared duration update loop — starts a rAF loop that updates duration\n // from performance.now(). Used by both startRecording and resumeRecording.\n const startDurationLoop = useCallback(() => {\n const tick = () => {\n if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(tick);\n }\n };\n tick();\n }, []);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getGlobalContext();\n // Use addRecordingWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addRecordingWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels, done } = event.data as {\n channels: Float32Array[];\n done?: boolean;\n };\n\n // Empty messages can arrive from the final stop ack when nothing was buffered.\n // Handle channels first if present, then resolve the stop barrier.\n if (channels && channels.length > 0 && channels[0] && channels[0].length > 0) {\n for (let ch = 0; ch < channels.length; ch++) {\n if (!recordedChunksRef.current[ch]) {\n console.warn(\n `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`\n );\n recordedChunksRef.current[ch] = [];\n }\n recordedChunksRef.current[ch].push(channels[ch]);\n }\n // Capture sample offset before incrementing — used by peak alignment\n const samplesProcessedBefore = totalSamplesRef.current;\n totalSamplesRef.current += channels[0].length;\n\n // Skip live-preview peak updates while stop is in flight. The\n // queue may have many flushes pending; updating peaks for each\n // would let the live-preview waveform grow visibly past where\n // the user stopped, only to be replaced moments later by the\n // final clip (which is shorter due to latency compensation).\n // Chunks are already pushed above so no audio is lost.\n if (stopAckResolveRef.current === null) {\n setPeaks((prevPeaks) => {\n // Ensure we have an entry per channel\n const updated: (Int8Array | Int16Array)[] = [];\n for (let ch = 0; ch < channels.length; ch++) {\n const prev = prevPeaks[ch] ?? emptyPeaks(bits);\n updated.push(\n appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)\n );\n }\n return updated;\n });\n }\n }\n\n if (done) {\n stopAckResolveRef.current?.();\n stopAckResolveRef.current = null;\n }\n\n // Note: VU meter levels come from useMicrophoneLevel (meter-processor worklet)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Connect and start — after state reset and handler setup\n source.connect(workletNode);\n // Do NOT send sampleRate — worklet uses its global sampleRate (always correct)\n workletNode.port.postMessage({ command: 'start', channelCount: streamChannelCount });\n\n // Listen on MediaStreamTrack for mic unplug (MediaStream has no 'ended' event)\n const audioTrack = stream.getAudioTracks()[0] ?? null;\n audioTrackRef.current = audioTrack;\n if (audioTrack) {\n const onEnded = () => {\n console.warn('[waveform-playlist] Audio track ended (mic unplugged or revoked)');\n // Stop recording to clean up worklet, rAF loop, and UI state\n stopRecordingRef.current?.();\n setError(new Error('Microphone disconnected during recording'));\n };\n onTrackEndedRef.current = onEnded;\n audioTrack.addEventListener('ended', onEnded);\n }\n\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n startDurationLoop();\n } catch (err) {\n console.warn('[waveform-playlist] Failed to start recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecordingRef.current) {\n return null;\n }\n\n try {\n // Freeze the duration tick immediately so the live-preview width\n // (durationSamples = duration * sampleRate) doesn't keep growing\n // during the worklet stop handshake / queue drain. isStoppingRef\n // stops the rAF tick from rescheduling; cancelling animationFrameRef\n // kills any frame already queued.\n isStoppingRef.current = true;\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n audioTrackRef.current = null;\n onTrackEndedRef.current = null;\n }\n\n // Stop the worklet and wait for its final flush message before reading chunks.\n // Without the await, the partial buffer at the end of recording arrives after\n // the AudioBuffer is built and is silently dropped.\n // Snapshot the worklet node once — the drain loop yields the event loop\n // for up to 250ms, during which an unmount-cleanup could null the ref.\n const node = workletNodeRef.current;\n if (node) {\n const stopAck = new Promise<void>((resolve) => {\n stopAckResolveRef.current = resolve;\n });\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n // Safety timeout (1s) — purely a circuit breaker for a truly failed\n // worklet (crashed, context closed). The drain loop below catches\n // any straggler flush messages that were queued before the timeout\n // fired, so chunks are complete even when this fires.\n const timeout = new Promise<void>((resolve) => {\n timeoutId = setTimeout(resolve, 1000);\n });\n\n node.port.postMessage({ command: 'stop' });\n await Promise.race([stopAck, timeout]);\n clearTimeout(timeoutId);\n stopAckResolveRef.current = null;\n\n // Drain the event-loop queue. During recording the worklet posts a\n // flush every ~16ms; if main was slower than that, messages back up.\n // Yield repeatedly until totalSamplesRef stabilizes for several\n // consecutive ticks — at that point the queue is empty.\n let lastSamples = -1;\n let stable = 0;\n for (let i = 0; i < 50; i++) {\n if (totalSamplesRef.current === lastSamples) {\n if (++stable >= 3) break;\n } else {\n stable = 0;\n lastSamples = totalSamplesRef.current;\n }\n await new Promise<void>((r) => setTimeout(r, 5));\n }\n\n // Null the handler so any late delivery from this worklet doesn't\n // contaminate a subsequent recording session's chunks.\n node.port.onmessage = null;\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(node);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during stop:', String(err));\n }\n }\n node.disconnect();\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to stop recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n } finally {\n // Always clear recording flags, even on failure — otherwise the UI gets\n // stuck in \"recording\" state if AudioBuffer creation throws.\n isRecordingRef.current = false;\n isPausedRef.current = false;\n isStoppingRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n workletNodeRef.current?.port.postMessage({ command: 'pause' });\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n workletNodeRef.current?.port.postMessage({ command: 'resume' });\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n startDurationLoop();\n }\n }, [isRecording, isPaused, duration, startDurationLoop]);\n\n // Cleanup on unmount — read refs in cleanup, not effect body.\n // Pattern #16 (copy refs in effect body) doesn't apply to [] deps — refs are\n // null at mount and only set later during startRecording.\n useEffect(() => {\n return () => {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n }\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during cleanup:', String(err));\n }\n }\n try {\n workletNodeRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Worklet disconnect during cleanup:', String(err));\n }\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(\n async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(err instanceof Error ? err : new Error('Failed to access microphone'));\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n },\n [stream, enumerateDevices]\n );\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state, enumerate devices, and listen for hot-plug changes\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Re-enumerate when devices are plugged in or removed\n navigator.mediaDevices.addEventListener('devicechange', enumerateDevices);\n\n // Cleanup on unmount\n return () => {\n navigator.mediaDevices.removeEventListener('devicechange', enumerateDevices);\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, [enumerateDevices, stream]);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses an AudioWorklet-based meter processor for sample-accurate\n * peak and RMS metering without requestAnimationFrame overhead.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { addMeterWorkletModule, 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 UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = 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 const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Use addMeterWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addMeterWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\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: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).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[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 // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', 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 (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport {\n type ClipTrack,\n type AudioClip,\n resolveRecordingOffsetSamples,\n} from '@waveform-playlist/core';\nimport {\n resumeGlobalAudioContext,\n getGlobalAudioContext,\n getGlobalContext,\n} from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n\n /**\n * Latency offset to skip at the start of the recording, in **seconds**.\n * Absolute replacement for the auto-computed `outputLatency + lookAhead` value\n * — use this to apply an externally-measured round-trip latency. `0` disables\n * compensation; when omitted, the auto-computed value is used. Pass the same\n * value into the provider's `recordingState.latencyOffset` so the live preview\n * matches the finalized clip. Negative or non-finite values are treated as 0.\n */\n latencyOffset?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n /** Per-channel peak levels (0-1). Array length matches channelCount. */\n levels: number[];\n /** Per-channel held peak levels (0-1). Array length matches channelCount. */\n peakLevels: number[];\n /** Per-channel RMS levels (0-1). Array length matches channelCount. */\n rmsLevels: number[];\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: (Int8Array | Int16Array)[];\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, latencyOffset, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Capture timeline position when recording starts (not at stop time)\n const recordingStartTimeRef = useRef(0);\n\n // Keep selectedTrackId and currentTime in refs for use in callbacks.\n // Avoids stale closures and prevents 60fps useCallback recreation\n // (currentTime updates at animation-frame rate during playback).\n const selectedTrackIdRef = useRef(selectedTrackId);\n selectedTrackIdRef.current = selectedTrackId;\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n\n // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const {\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n resetPeak,\n error: meterError,\n } = useMicrophoneLevel(stream, {\n channelCount: recordingOptions.channelCount,\n });\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _recordedAudioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n // Reads selectedTrackId from ref to avoid stale closures when\n // auto-create track + start recording happen in the same render cycle\n const startRecording = useCallback(async () => {\n if (!selectedTrackIdRef.current) {\n setHookError(\n new Error('Cannot start recording: no track selected. Select or create a track first.')\n );\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n // Capture timeline position NOW — before recording starts.\n // Using currentTime at stop time would be wrong during overdub\n // (playback advances currentTime while recording).\n recordingStartTimeRef.current = currentTimeRef.current;\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n const trackId = selectedTrackIdRef.current;\n if (buffer && trackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === trackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${trackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.warn(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Use the captured start time (not live currentTime which advances during overdub)\n const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);\n\n // Latency compensation: an explicit latencyOffset (seconds) replaces the\n // auto-computed outputLatency + lookAhead window. Shared resolver with the\n // live preview in PlaylistVisualization — keep both using the same value so\n // trim widths match.\n const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const latencyOffsetSamples = resolveRecordingOffsetSamples({\n overrideSeconds: latencyOffset,\n outputLatency,\n lookAhead,\n sampleRate: buffer.sampleRate,\n });\n\n // Guard: very short recordings (< latency compensation) would produce negative duration\n const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);\n if (effectiveDuration === 0) {\n console.warn(\n '[waveform-playlist] Recording too short for latency compensation — discarding'\n );\n setHookError(new Error('Recording was too short to save. Try recording for longer.'));\n return;\n }\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: effectiveDuration,\n offsetSamples: latencyOffsetSamples,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [tracks, setTracks, stopRec, latencyOffset]);\n\n // Auto-select first device when available, or fallback if selected device was unplugged\n useEffect(() => {\n if (!hasPermission || devices.length === 0) return;\n\n if (selectedDevice === null) {\n // First-time selection\n setSelectedDevice(devices[0].deviceId);\n } else if (!devices.some((d) => d.deviceId === selectedDevice)) {\n // Selected device was removed — fall back to first available\n const fallbackId = devices[0].deviceId;\n setSelectedDevice(fallbackId);\n resetPeak();\n requestAccess(fallbackId, audioConstraints);\n }\n }, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(\n async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n resetPeak();\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [requestAccess, audioConstraints, resetPeak]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n error: hookError || micError || meterError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n"],"mappings":"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","names":["error","useState","useEffect","useCallback","useEffect","useState","useRef","useCallback","getGlobalContext","useState","useCallback","useEffect","useRef","getGlobalContext","useState","useRef","useCallback","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer, appendPeaks } from '@waveform-playlist/core';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { addRecordingWorkletModule } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n // Resolved when the worklet posts the final done message after a stop command.\n const stopAckResolveRef = useRef<(() => void) | null>(null);\n // True from the start of stopRecording until it resolves. Distinct from\n // isPausedRef (user explicitly paused) — both freeze the duration tick,\n // but conflating them risks pauseRecording becoming a no-op while we're\n // stopping, or vice versa.\n const isStoppingRef = useRef<boolean>(false);\n // In-flight stop result — re-entrant stopRecording calls await this\n // instead of running a second handshake.\n const stopPromiseRef = useRef<Promise<AudioBuffer | null> | null>(null);\n // Assigned each render below (same pattern as stopRecordingRef) so the\n // stable stopRecording callback can invoke the handshake implementation.\n const finishStopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n // Set synchronously at startRecording entry. isRecordingRef only flips\n // after several awaits — without this, two rapid start calls both pass\n // the guard and build two capture pipelines feeding one chunk store.\n const isStartingRef = useRef<boolean>(false);\n // Set by the unmount cleanup so a startRecording continuation that was\n // parked on an await doesn't build an ownerless capture graph.\n const isUnmountedRef = useRef<boolean>(false);\n // Mirrors the duration state for callbacks that must not close over it.\n const durationRef = useRef<number>(0);\n\n // Shared duration update loop — starts a rAF loop that updates duration\n // from performance.now(). Used by both startRecording and resumeRecording.\n const startDurationLoop = useCallback(() => {\n const tick = () => {\n if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n durationRef.current = elapsed;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(tick);\n }\n };\n tick();\n }, []);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getGlobalContext();\n // Use addRecordingWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addRecordingWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording. Resolves true when the capture pipeline started.\n const startRecording = useCallback(async (): Promise<boolean> => {\n if (isRecordingRef.current || isStartingRef.current) return false;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return false;\n }\n isStartingRef.current = true;\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Unmounted while awaiting resume/module load — abort before any graph\n // nodes exist. A continuation past this point would start an ownerless\n // session: the rAF loop reschedules forever and chunks accumulate with\n // nothing left to stop them.\n if (isUnmountedRef.current) return false;\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels, done } = event.data as {\n channels: Float32Array[];\n done?: boolean;\n };\n\n // Empty messages can arrive from the final stop ack when nothing was buffered.\n // Handle channels first if present, then resolve the stop barrier.\n if (channels && channels.length > 0 && channels[0] && channels[0].length > 0) {\n for (let ch = 0; ch < channels.length; ch++) {\n if (!recordedChunksRef.current[ch]) {\n console.warn(\n `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`\n );\n recordedChunksRef.current[ch] = [];\n }\n recordedChunksRef.current[ch].push(channels[ch]);\n }\n // Capture sample offset before incrementing — used by peak alignment\n const samplesProcessedBefore = totalSamplesRef.current;\n totalSamplesRef.current += channels[0].length;\n\n // Skip live-preview peak updates while stop is in flight. The\n // queue may have many flushes pending; updating peaks for each\n // would let the live-preview waveform grow visibly past where\n // the user stopped, only to be replaced moments later by the\n // final clip (which is shorter due to latency compensation).\n // Chunks are already pushed above so no audio is lost.\n if (stopAckResolveRef.current === null) {\n setPeaks((prevPeaks) => {\n // Ensure we have an entry per channel\n const updated: (Int8Array | Int16Array)[] = [];\n for (let ch = 0; ch < channels.length; ch++) {\n const prev = prevPeaks[ch] ?? emptyPeaks(bits);\n updated.push(\n appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)\n );\n }\n return updated;\n });\n }\n }\n\n if (done) {\n stopAckResolveRef.current?.();\n stopAckResolveRef.current = null;\n }\n\n // Note: VU meter levels come from useMicrophoneLevel (meter-processor worklet)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Connect and start — after state reset and handler setup\n source.connect(workletNode);\n // Do NOT send sampleRate — worklet uses its global sampleRate (always correct)\n workletNode.port.postMessage({ command: 'start', channelCount: streamChannelCount });\n\n // Listen on MediaStreamTrack for mic unplug (MediaStream has no 'ended' event)\n const audioTrack = stream.getAudioTracks()[0] ?? null;\n audioTrackRef.current = audioTrack;\n if (audioTrack) {\n const onEnded = () => {\n console.warn('[waveform-playlist] Audio track ended (mic unplugged or revoked)');\n // Stop recording to clean up worklet, rAF loop, and UI state\n stopRecordingRef.current?.();\n setError(new Error('Microphone disconnected during recording'));\n };\n onTrackEndedRef.current = onEnded;\n audioTrack.addEventListener('ended', onEnded);\n }\n\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n startDurationLoop();\n return true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to start recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n return false;\n } finally {\n isStartingRef.current = false;\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecordingRef.current) {\n return null;\n }\n // Re-entrant stop (double-click, unmount cleanup while a stop is\n // pending): a second handshake would overwrite the ack resolver —\n // stranding the first call on its safety timeout — and post a redundant\n // stop command. Both callers observe the same in-flight stop.\n if (isStoppingRef.current) {\n return stopPromiseRef.current ?? null;\n }\n // Freeze the duration tick immediately so the live-preview width\n // (durationSamples = duration * sampleRate) doesn't keep growing\n // during the worklet stop handshake / queue drain. isStoppingRef\n // stops the rAF tick from rescheduling; cancelling animationFrameRef\n // kills any frame already queued.\n isStoppingRef.current = true;\n const stopPromise = finishStopRecordingRef.current!();\n stopPromiseRef.current = stopPromise;\n return stopPromise;\n }, []);\n\n // The actual stop handshake — only ever invoked via stopRecording's\n // re-entrancy gate above.\n const finishStopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n try {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n audioTrackRef.current = null;\n onTrackEndedRef.current = null;\n }\n\n // Stop the worklet and wait for its final flush message before reading chunks.\n // Without the await, the partial buffer at the end of recording arrives after\n // the AudioBuffer is built and is silently dropped.\n // Snapshot the worklet node once — the drain loop yields the event loop\n // for up to 250ms, during which an unmount-cleanup could null the ref.\n const node = workletNodeRef.current;\n if (node) {\n const stopAck = new Promise<void>((resolve) => {\n stopAckResolveRef.current = resolve;\n });\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n // Safety timeout (1s) — purely a circuit breaker for a truly failed\n // worklet (crashed, context closed). The drain loop below catches\n // any straggler flush messages that were queued before the timeout\n // fired, so chunks are complete even when this fires.\n const timeout = new Promise<void>((resolve) => {\n timeoutId = setTimeout(resolve, 1000);\n });\n\n node.port.postMessage({ command: 'stop' });\n await Promise.race([stopAck, timeout]);\n clearTimeout(timeoutId);\n stopAckResolveRef.current = null;\n\n // Drain the event-loop queue. During recording the worklet posts a\n // flush every ~16ms; if main was slower than that, messages back up.\n // Yield repeatedly until totalSamplesRef stabilizes for several\n // consecutive ticks — at that point the queue is empty.\n let lastSamples = -1;\n let stable = 0;\n for (let i = 0; i < 50; i++) {\n if (totalSamplesRef.current === lastSamples) {\n if (++stable >= 3) break;\n } else {\n stable = 0;\n lastSamples = totalSamplesRef.current;\n }\n await new Promise<void>((r) => setTimeout(r, 5));\n }\n\n // Null the handler so any late delivery from this worklet doesn't\n // contaminate a subsequent recording session's chunks.\n node.port.onmessage = null;\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(node);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during stop:', String(err));\n }\n }\n node.disconnect();\n\n // Session fully torn down — null the refs so the unmount cleanup\n // doesn't post a redundant stop to the already-stopped worklet.\n workletNodeRef.current = null;\n mediaStreamSourceRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n durationRef.current = buffer.duration;\n setDuration(buffer.duration);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to stop recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n } finally {\n // Always clear recording flags, even on failure — otherwise the UI gets\n // stuck in \"recording\" state if AudioBuffer creation throws.\n isRecordingRef.current = false;\n isPausedRef.current = false;\n isStoppingRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n finishStopRecordingRef.current = finishStopRecording;\n\n // Pause recording — guards read refs, not state: a pause in the same tick\n // as start/resume closes over stale state and silently no-ops while the\n // worklet keeps capturing (only visible in the resulting buffer length).\n const pauseRecording = useCallback(() => {\n if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {\n workletNodeRef.current?.port.postMessage({ command: 'pause' });\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, []);\n\n // Resume recording — same ref-based guards as pauseRecording.\n const resumeRecording = useCallback(() => {\n if (isRecordingRef.current && isPausedRef.current && !isStoppingRef.current) {\n workletNodeRef.current?.port.postMessage({ command: 'resume' });\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - durationRef.current * 1000;\n startDurationLoop();\n }\n }, [startDurationLoop]);\n\n // Cleanup on unmount — read refs in cleanup, not effect body.\n // Pattern #16 (copy refs in effect body) doesn't apply to [] deps — refs are\n // null at mount and only set later during startRecording.\n useEffect(() => {\n // Reset on (re)mount — StrictMode dev remounts reuse the same hook\n // instance, so a flag left true by the probe-unmount's cleanup would\n // make startRecording abort forever.\n isUnmountedRef.current = false;\n return () => {\n // Abort any startRecording continuation parked on an await — it must\n // not build the capture graph after unmount.\n isUnmountedRef.current = true;\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n }\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during cleanup:', String(err));\n }\n }\n try {\n workletNodeRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Worklet disconnect during cleanup:', String(err));\n }\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback, useRef } from 'react';\nimport { enumerateMicrophones } from '@waveform-playlist/core';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Mirrors the stream state so unmount cleanup and overlapping requests\n // operate on the CURRENT stream, not a stale closure capture.\n const streamRef = useRef<MediaStream | null>(null);\n // Generation token: bumped by every requestAccess (and by stopStream /\n // unmount). A getUserMedia grant belonging to an older generation is a\n // stale request — its stream must be stopped, not installed, or the mic\n // stays hot with no owner (permission prompt resolving after unmount,\n // or the slower of two overlapping requests).\n const requestGenerationRef = useRef(0);\n // Set by unmount cleanup (plain assignment — the exhaustive-deps rule\n // rejects read-modify-writes like a generation bump in cleanup functions).\n const isUnmountedRef = useRef(false);\n\n // Enumerate audio input devices — delegates to the framework-agnostic core\n // helper (redaction-aware fallback labels, SSR/insecure-context safe).\n const enumerateDevices = useCallback(async () => {\n try {\n const { devices: audioInputs } = await enumerateMicrophones();\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(\n async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n const generation = ++requestGenerationRef.current;\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop and clear the existing stream. Clearing state (not just\n // stopping tracks) matters on failure: consumers left holding a\n // stopped stream silently record silence with no error.\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n setStream(null);\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n\n // Superseded by a newer request, stopStream, or unmount while the\n // permission prompt was open — stop the just-granted stream instead\n // of installing it (a hot mic nobody owns).\n if (generation !== requestGenerationRef.current || isUnmountedRef.current) {\n newStream.getTracks().forEach((track) => track.stop());\n return;\n }\n\n streamRef.current = newStream;\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n if (generation === requestGenerationRef.current) {\n setError(err instanceof Error ? err : new Error('Failed to access microphone'));\n setHasPermission(false);\n }\n } finally {\n if (generation === requestGenerationRef.current) {\n setIsLoading(false);\n }\n }\n },\n [enumerateDevices]\n );\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n // Invalidate any in-flight request so a pending grant can't re-arm the mic\n requestGenerationRef.current++;\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n setStream(null);\n setHasPermission(false);\n }\n }, []);\n\n // Check initial permission state, enumerate devices, and listen for hot-plug changes\n useEffect(() => {\n // Reset on (re)mount — StrictMode dev remounts reuse the same hook\n // instance, so a flag left true by the probe-unmount's cleanup would\n // stop every future getUserMedia grant as \"stale\".\n isUnmountedRef.current = false;\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Re-enumerate when devices are plugged in or removed\n navigator.mediaDevices.addEventListener('devicechange', enumerateDevices);\n\n // Cleanup on unmount\n return () => {\n navigator.mediaDevices.removeEventListener('devicechange', enumerateDevices);\n // Invalidate any in-flight request (a grant arriving after unmount is\n // stopped by the isUnmounted check) and stop the current stream.\n isUnmountedRef.current = true;\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }\n };\n }, [enumerateDevices]);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses an AudioWorklet-based meter processor for sample-accurate\n * peak and RMS metering without requestAnimationFrame overhead.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { addMeterWorkletModule, 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 UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches the metered channel\n * count: the mic's auto-detected channels, with mono mirrored up to the\n * requested `channelCount`. A mic with MORE channels than `channelCount`\n * yields one entry per actual channel.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Same length contract as `levels`.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Same length contract as `levels`.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = 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 const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Use addMeterWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addMeterWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\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: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n // MeterMessage layout: [0..N-1] peak, [N..2N-1] rms\n const data = event.data as MeterMessage;\n const workletChannels = data.length / 2;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < workletChannels; ch++) {\n smoothed[ch] = Math.max(data[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(data[workletChannels + ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n workletChannels < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n workletChannels < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n\n // Setup succeeded — clear any error from a previous failed attempt so\n // the UI doesn't show a permanent failure while metering works.\n setMeterError(null);\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', 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 (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport {\n type ClipTrack,\n type AudioClip,\n carveClipRange,\n resolveRecordingOffsetSamples,\n} from '@waveform-playlist/core';\nimport {\n resumeGlobalAudioContext,\n getGlobalAudioContext,\n getGlobalContext,\n} from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds.\n * Punch-in semantics (#579): the recorded clip lands exactly at this\n * position and REPLACES any existing clip content it overlaps — partial\n * overlaps are trimmed, fully-covered clips removed, spanning clips split.\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n\n /**\n * Latency offset to skip at the start of the recording, in **seconds**.\n * Absolute replacement for the auto-computed `outputLatency + lookAhead` value\n * — use this to apply an externally-measured round-trip latency. `0` disables\n * compensation; when omitted, the auto-computed value is used. Pass the same\n * value into the provider's `recordingState.latencyOffset` so the live preview\n * matches the finalized clip. Negative or non-finite values are treated as 0.\n */\n latencyOffset?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n /**\n * Per-channel peak levels (0-1). Array length matches the metered channel\n * count (auto-detected mic channels; mono mirrored up to channelCount —\n * see UseMicrophoneLevelReturn.levels).\n */\n levels: number[];\n /** Per-channel held peak levels (0-1). Same length contract as `levels`. */\n peakLevels: number[];\n /** Per-channel RMS levels (0-1). Same length contract as `levels`. */\n rmsLevels: number[];\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n /** Start capturing on the selected track. Resolves `true` when the capture\n * pipeline actually started — callers that synchronize playback with\n * recording (overdub) should check this before starting playback. */\n startRecording: () => Promise<boolean>;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: (Int8Array | Int16Array)[];\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, latencyOffset, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Capture timeline position when recording starts (not at stop time)\n const recordingStartTimeRef = useRef(0);\n\n // Keep selectedTrackId and currentTime in refs for use in callbacks.\n // Avoids stale closures and prevents 60fps useCallback recreation\n // (currentTime updates at animation-frame rate during playback).\n const selectedTrackIdRef = useRef(selectedTrackId);\n selectedTrackIdRef.current = selectedTrackId;\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n // Latest tracks for reads that happen AFTER an await — the stop handshake\n // can take >1s, and finalizing against a closure capture would discard any\n // track edits that landed in that window.\n const tracksRef = useRef(tracks);\n tracksRef.current = tracks;\n\n // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const {\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n resetPeak,\n error: meterError,\n } = useMicrophoneLevel(stream, {\n channelCount: recordingOptions.channelCount,\n });\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _recordedAudioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n // Reads selectedTrackId from ref to avoid stale closures when\n // auto-create track + start recording happen in the same render cycle\n const startRecording = useCallback(async (): Promise<boolean> => {\n if (!selectedTrackIdRef.current) {\n setHookError(\n new Error('Cannot start recording: no track selected. Select or create a track first.')\n );\n return false;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n // Capture timeline position NOW — before recording starts.\n // Using currentTime at stop time would be wrong during overdub\n // (playback advances currentTime while recording).\n recordingStartTimeRef.current = currentTimeRef.current;\n\n return await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return false;\n }\n }, [isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes. Read tracks through the\n // ref — the awaited stop handshake above can span >1s, and the closure's\n // tracks capture would silently revert concurrent edits.\n const trackId = selectedTrackIdRef.current;\n const currentTracks = tracksRef.current;\n if (buffer && trackId) {\n const selectedTrackIndex = currentTracks.findIndex((t) => t.id === trackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${trackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.warn(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n // Use the captured start time (not live currentTime which advances during overdub).\n // Punch-in replace (#579): the take lands exactly here; overlapped\n // content is carved out below when the clip list is assembled.\n const startSample = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);\n\n // Latency compensation: an explicit latencyOffset (seconds) replaces the\n // auto-computed outputLatency + lookAhead window. Shared resolver with the\n // live preview in PlaylistVisualization — keep both using the same value so\n // trim widths match.\n const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const latencyOffsetSamples = resolveRecordingOffsetSamples({\n overrideSeconds: latencyOffset,\n outputLatency,\n lookAhead,\n sampleRate: buffer.sampleRate,\n });\n\n // Guard: very short recordings (< latency compensation) would produce negative duration\n const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);\n if (effectiveDuration === 0) {\n console.warn(\n '[waveform-playlist] Recording too short for latency compensation — discarding'\n );\n setHookError(new Error('Recording was too short to save. Try recording for longer.'));\n return;\n }\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: effectiveDuration,\n offsetSamples: latencyOffsetSamples,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track — the recorded clip owns [startSample,\n // startSample + effectiveDuration): trim/remove/split whatever it overlaps.\n const newTracks = currentTracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [\n ...carveClipRange(track.clips, startSample, startSample + effectiveDuration),\n newClip,\n ],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [setTracks, stopRec, latencyOffset]);\n\n // Auto-select first device when available, or fallback if selected device was unplugged\n useEffect(() => {\n if (!hasPermission || devices.length === 0) return;\n\n if (selectedDevice === null) {\n // First-time selection\n setSelectedDevice(devices[0].deviceId);\n } else if (!devices.some((d) => d.deviceId === selectedDevice)) {\n // Selected device was removed — fall back to first available\n const fallbackId = devices[0].deviceId;\n setSelectedDevice(fallbackId);\n resetPeak();\n requestAccess(fallbackId, audioConstraints);\n }\n }, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);\n\n // Mirror isRecording for the device-switch guards below — reading the\n // state directly would churn the callbacks every recording toggle.\n const isRecordingRef = useRef(isRecording);\n isRecordingRef.current = isRecording;\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n // requestAccess stops the current stream's tracks, and track.stop() fires\n // no 'ended' event — the recording's source is never rewired, so the rest\n // of the take silently records silence. Refuse while recording.\n if (isRecordingRef.current) {\n setHookError(new Error('Cannot request microphone access while recording. Stop first.'));\n return;\n }\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(\n async (deviceId: string) => {\n // Same silent-silence hazard as requestMicAccess above.\n if (isRecordingRef.current) {\n setHookError(new Error('Cannot switch microphone while recording. Stop first.'));\n return;\n }\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n resetPeak();\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [requestAccess, audioConstraints, resetPeak]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n error: hookError || micError || meterError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n"],"mappings":";AAIA,SAAS,UAAU,QAAQ,aAAa,iBAAiB;AAEzD,SAAS,sBAAsB,mBAAmB,mBAAmB;AACrE,SAAS,wBAAwB;AACjC,SAAS,iCAAiC;AAE1C,SAAS,WAAW,MAAsC;AACxD,SAAO,SAAS,IAAI,IAAI,UAAU,CAAC,IAAI,IAAI,WAAW,CAAC;AACzD;AAEO,SAAS,aACd,QACA,UAA4B,CAAC,GACT;AACpB,QAAM,EAAE,eAAe,GAAG,kBAAkB,MAAM,OAAO,GAAG,IAAI;AAGhE,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AACpD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,CAAC;AAE1C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAqC,CAAC,WAAW,IAAI,CAAC,CAAC;AACjF,QAAM,CAAC,aAAa,cAAc,IAAI,SAA6B,IAAI;AACvE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,CAAC;AACpC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,CAAC;AAK5C,QAAM,mBAAmB,OAAgB,KAAK;AAG9C,QAAM,iBAAiB,OAAgC,IAAI;AAC3D,QAAM,uBAAuB,OAA0C,IAAI;AAE3E,QAAM,oBAAoB,OAAyB,CAAC,CAAC;AACrD,QAAM,kBAAkB,OAAO,CAAC;AAChC,QAAM,oBAAoB,OAAsB,IAAI;AACpD,QAAM,eAAe,OAAe,CAAC;AACrC,QAAM,iBAAiB,OAAgB,KAAK;AAC5C,QAAM,cAAc,OAAgB,KAAK;AACzC,QAAM,gBAAgB,OAAgC,IAAI;AAC1D,QAAM,kBAAkB,OAA4B,IAAI;AACxD,QAAM,mBAAmB,OAAmD,IAAI;AAEhF,QAAM,oBAAoB,OAA4B,IAAI;AAK1D,QAAM,gBAAgB,OAAgB,KAAK;AAG3C,QAAM,iBAAiB,OAA2C,IAAI;AAGtE,QAAM,yBAAyB,OAAmD,IAAI;AAItF,QAAM,gBAAgB,OAAgB,KAAK;AAG3C,QAAM,iBAAiB,OAAgB,KAAK;AAE5C,QAAM,cAAc,OAAe,CAAC;AAIpC,QAAM,oBAAoB,YAAY,MAAM;AAC1C,UAAM,OAAO,MAAM;AACjB,UAAI,eAAe,WAAW,CAAC,YAAY,WAAW,CAAC,cAAc,SAAS;AAC5E,cAAM,WAAW,YAAY,IAAI,IAAI,aAAa,WAAW;AAC7D,oBAAY,UAAU;AACtB,oBAAY,OAAO;AACnB,0BAAkB,UAAU,sBAAsB,IAAI;AAAA,MACxD;AAAA,IACF;AACA,SAAK;AAAA,EACP,GAAG,CAAC,CAAC;AAGL,QAAM,cAAc,YAAY,YAAY;AAE1C,QAAI,iBAAiB,SAAS;AAC5B;AAAA,IACF;AAEA,QAAI;AACF,YAAM,UAAU,iBAAiB;AAIjC,YAAM,SAAS,QAAQ;AACvB,YAAM,0BAA0B,CAAC,QAAQ,OAAO,aAAa,UAAU,GAAG,CAAC;AAC3E,uBAAiB,UAAU;AAAA,IAC7B,SAAS,KAAK;AACZ,cAAQ,KAAK,2DAA2D,OAAO,GAAG,CAAC;AACnF,YAAMA,SAAQ,IAAI,MAAM,yCAAyC,OAAO,GAAG,CAAC;AAC5E,YAAMA;AAAA,IACR;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,iBAAiB,YAAY,YAA8B;AAC/D,QAAI,eAAe,WAAW,cAAc,QAAS,QAAO;AAC5D,QAAI,CAAC,QAAQ;AACX,eAAS,IAAI,MAAM,gCAAgC,CAAC;AACpD,aAAO;AAAA,IACT;AACA,kBAAc,UAAU;AAExB,QAAI;AACF,eAAS,IAAI;AAGb,YAAM,UAAU,iBAAiB;AAGjC,UAAI,QAAQ,UAAU,aAAa;AACjC,cAAM,QAAQ,OAAO;AAAA,MACvB;AAGA,YAAM,YAAY;AAMlB,UAAI,eAAe,QAAS,QAAO;AAInC,YAAM,SAAS,QAAQ,wBAAwB,MAAM;AACrD,2BAAqB,UAAU;AAK/B,YAAM,uBAAuB,OAAO,eAAe,EAAE,CAAC,GAAG,YAAY,EAAE;AACvE,UAAI,yBAAyB,QAAW;AACtC,gBAAQ;AAAA,UACN,8EAA8E,YAAY;AAAA,QAC5F;AAAA,MACF;AACA,YAAM,qBAAqB,wBAAwB;AAEnD,YAAM,cAAc,QAAQ,uBAAuB,uBAAuB;AAAA,QACxE,cAAc;AAAA,QACd,kBAAkB;AAAA,MACpB,CAAC;AACD,qBAAe,UAAU;AAEzB,kBAAY,mBAAmB,CAAC,UAAU;AACxC,gBAAQ,KAAK,0DAA0D,OAAO,KAAK,CAAC;AACpF,iBAAS,IAAI,MAAM,0CAA0C,CAAC;AAAA,MAChE;AAIA,wBAAkB,UAAU,MAAM,KAAK,EAAE,QAAQ,mBAAmB,GAAG,MAAM,CAAC,CAAC;AAC/E,sBAAgB,UAAU;AAC1B,eAAS,MAAM,KAAK,EAAE,QAAQ,mBAAmB,GAAG,MAAM,WAAW,IAAI,CAAC,CAAC;AAC3E,qBAAe,IAAI;AACnB,eAAS,CAAC;AACV,mBAAa,CAAC;AAGd,kBAAY,KAAK,YAAY,CAAC,UAAwB;AACpD,cAAM,EAAE,UAAU,KAAK,IAAI,MAAM;AAOjC,YAAI,YAAY,SAAS,SAAS,KAAK,SAAS,CAAC,KAAK,SAAS,CAAC,EAAE,SAAS,GAAG;AAC5E,mBAAS,KAAK,GAAG,KAAK,SAAS,QAAQ,MAAM;AAC3C,gBAAI,CAAC,kBAAkB,QAAQ,EAAE,GAAG;AAClC,sBAAQ;AAAA,gBACN,0CAA0C,EAAE,2BAA2B,kBAAkB,QAAQ,MAAM;AAAA,cACzG;AACA,gCAAkB,QAAQ,EAAE,IAAI,CAAC;AAAA,YACnC;AACA,8BAAkB,QAAQ,EAAE,EAAE,KAAK,SAAS,EAAE,CAAC;AAAA,UACjD;AAEA,gBAAM,yBAAyB,gBAAgB;AAC/C,0BAAgB,WAAW,SAAS,CAAC,EAAE;AAQvC,cAAI,kBAAkB,YAAY,MAAM;AACtC,qBAAS,CAAC,cAAc;AAEtB,oBAAM,UAAsC,CAAC;AAC7C,uBAAS,KAAK,GAAG,KAAK,SAAS,QAAQ,MAAM;AAC3C,sBAAM,OAAO,UAAU,EAAE,KAAK,WAAW,IAAI;AAC7C,wBAAQ;AAAA,kBACN,YAAY,MAAM,SAAS,EAAE,GAAG,iBAAiB,wBAAwB,IAAI;AAAA,gBAC/E;AAAA,cACF;AACA,qBAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,QACF;AAEA,YAAI,MAAM;AACR,4BAAkB,UAAU;AAC5B,4BAAkB,UAAU;AAAA,QAC9B;AAAA,MAIF;AAGA,aAAO,QAAQ,WAAW;AAE1B,kBAAY,KAAK,YAAY,EAAE,SAAS,SAAS,cAAc,mBAAmB,CAAC;AAGnF,YAAM,aAAa,OAAO,eAAe,EAAE,CAAC,KAAK;AACjD,oBAAc,UAAU;AACxB,UAAI,YAAY;AACd,cAAM,UAAU,MAAM;AACpB,kBAAQ,KAAK,kEAAkE;AAE/E,2BAAiB,UAAU;AAC3B,mBAAS,IAAI,MAAM,0CAA0C,CAAC;AAAA,QAChE;AACA,wBAAgB,UAAU;AAC1B,mBAAW,iBAAiB,SAAS,OAAO;AAAA,MAC9C;AAEA,qBAAe,UAAU;AACzB,kBAAY,UAAU;AACtB,qBAAe,IAAI;AACnB,kBAAY,KAAK;AACjB,mBAAa,UAAU,YAAY,IAAI;AACvC,wBAAkB;AAClB,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,cAAQ,KAAK,kDAAkD,OAAO,GAAG,CAAC;AAC1E,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,2BAA2B,CAAC;AAC5E,aAAO;AAAA,IACT,UAAE;AACA,oBAAc,UAAU;AAAA,IAC1B;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,iBAAiB,MAAM,aAAa,iBAAiB,CAAC;AAGhF,QAAM,gBAAgB,YAAY,YAAyC;AACzE,QAAI,CAAC,eAAe,SAAS;AAC3B,aAAO;AAAA,IACT;AAKA,QAAI,cAAc,SAAS;AACzB,aAAO,eAAe,WAAW;AAAA,IACnC;AAMA,kBAAc,UAAU;AACxB,UAAM,cAAc,uBAAuB,QAAS;AACpD,mBAAe,UAAU;AACzB,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAIL,QAAM,sBAAsB,YAAY,YAAyC;AAC/E,QAAI;AACF,UAAI,kBAAkB,YAAY,MAAM;AACtC,6BAAqB,kBAAkB,OAAO;AAC9C,0BAAkB,UAAU;AAAA,MAC9B;AAGA,UAAI,cAAc,WAAW,gBAAgB,SAAS;AACpD,sBAAc,QAAQ,oBAAoB,SAAS,gBAAgB,OAAO;AAC1E,sBAAc,UAAU;AACxB,wBAAgB,UAAU;AAAA,MAC5B;AAOA,YAAM,OAAO,eAAe;AAC5B,UAAI,MAAM;AACR,cAAM,UAAU,IAAI,QAAc,CAAC,YAAY;AAC7C,4BAAkB,UAAU;AAAA,QAC9B,CAAC;AACD,YAAI;AAKJ,cAAM,UAAU,IAAI,QAAc,CAAC,YAAY;AAC7C,sBAAY,WAAW,SAAS,GAAI;AAAA,QACtC,CAAC;AAED,aAAK,KAAK,YAAY,EAAE,SAAS,OAAO,CAAC;AACzC,cAAM,QAAQ,KAAK,CAAC,SAAS,OAAO,CAAC;AACrC,qBAAa,SAAS;AACtB,0BAAkB,UAAU;AAM5B,YAAI,cAAc;AAClB,YAAI,SAAS;AACb,iBAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,cAAI,gBAAgB,YAAY,aAAa;AAC3C,gBAAI,EAAE,UAAU,EAAG;AAAA,UACrB,OAAO;AACL,qBAAS;AACT,0BAAc,gBAAgB;AAAA,UAChC;AACA,gBAAM,IAAI,QAAc,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAAA,QACjD;AAIA,aAAK,KAAK,YAAY;AAGtB,YAAI,qBAAqB,SAAS;AAChC,cAAI;AACF,iCAAqB,QAAQ,WAAW,IAAI;AAAA,UAC9C,SAAS,KAAK;AACZ,oBAAQ,KAAK,sDAAsD,OAAO,GAAG,CAAC;AAAA,UAChF;AAAA,QACF;AACA,aAAK,WAAW;AAIhB,uBAAe,UAAU;AACzB,6BAAqB,UAAU;AAAA,MACjC;AAGA,YAAM,UAAU,iBAAiB;AACjC,YAAM,aAAa,QAAQ;AAC3B,YAAM,cAAc,kBAAkB,QAAQ,UAAU;AACxD,YAAM,cAAc,kBAAkB,QAAQ,IAAI,CAAC,WAAW,qBAAqB,MAAM,CAAC;AAC1F,YAAM,eAAe,YAAY,CAAC,GAAG,UAAU;AAI/C,UAAI,iBAAiB,GAAG;AACtB,gBAAQ,KAAK,iFAA4E;AACzF,eAAO;AAAA,MACT;AAEA,YAAM,SAAS,kBAAkB,YAAY,aAAa,WAAW,YAAY,WAAW;AAE5F,qBAAe,MAAM;AACrB,kBAAY,UAAU,OAAO;AAC7B,kBAAY,OAAO,QAAQ;AAG3B,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,cAAQ,KAAK,iDAAiD,OAAO,GAAG,CAAC;AACzE,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,0BAA0B,CAAC;AAC3E,aAAO;AAAA,IACT,UAAE;AAGA,qBAAe,UAAU;AACzB,kBAAY,UAAU;AACtB,oBAAc,UAAU;AACxB,qBAAe,KAAK;AACpB,kBAAY,KAAK;AACjB,eAAS,CAAC;AAAA,IACZ;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AACjB,mBAAiB,UAAU;AAC3B,yBAAuB,UAAU;AAKjC,QAAM,iBAAiB,YAAY,MAAM;AACvC,QAAI,eAAe,WAAW,CAAC,YAAY,WAAW,CAAC,cAAc,SAAS;AAC5E,qBAAe,SAAS,KAAK,YAAY,EAAE,SAAS,QAAQ,CAAC;AAC7D,UAAI,kBAAkB,YAAY,MAAM;AACtC,6BAAqB,kBAAkB,OAAO;AAC9C,0BAAkB,UAAU;AAAA,MAC9B;AACA,kBAAY,UAAU;AACtB,kBAAY,IAAI;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,kBAAkB,YAAY,MAAM;AACxC,QAAI,eAAe,WAAW,YAAY,WAAW,CAAC,cAAc,SAAS;AAC3E,qBAAe,SAAS,KAAK,YAAY,EAAE,SAAS,SAAS,CAAC;AAC9D,kBAAY,UAAU;AACtB,kBAAY,KAAK;AACjB,mBAAa,UAAU,YAAY,IAAI,IAAI,YAAY,UAAU;AACjE,wBAAkB;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,iBAAiB,CAAC;AAKtB,YAAU,MAAM;AAId,mBAAe,UAAU;AACzB,WAAO,MAAM;AAGX,qBAAe,UAAU;AAEzB,UAAI,cAAc,WAAW,gBAAgB,SAAS;AACpD,sBAAc,QAAQ,oBAAoB,SAAS,gBAAgB,OAAO;AAAA,MAC5E;AACA,UAAI,eAAe,SAAS;AAC1B,uBAAe,QAAQ,KAAK,YAAY,EAAE,SAAS,OAAO,CAAC;AAG3D,YAAI,qBAAqB,SAAS;AAChC,cAAI;AACF,iCAAqB,QAAQ,WAAW,eAAe,OAAO;AAAA,UAChE,SAAS,KAAK;AACZ,oBAAQ,KAAK,yDAAyD,OAAO,GAAG,CAAC;AAAA,UACnF;AAAA,QACF;AACA,YAAI;AACF,yBAAe,QAAQ,WAAW;AAAA,QACpC,SAAS,KAAK;AACZ,kBAAQ,KAAK,0DAA0D,OAAO,GAAG,CAAC;AAAA,QACpF;AAAA,MACF;AACA,UAAI,kBAAkB,YAAY,MAAM;AACtC,6BAAqB,kBAAkB,OAAO;AAAA,MAChD;AAAA,IAEF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC5dA,SAAS,YAAAC,WAAU,aAAAC,YAAW,eAAAC,cAAa,UAAAC,eAAc;AACzD,SAAS,4BAA4B;AAG9B,SAAS,sBAAiD;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAIH,UAA6B,IAAI;AAC7D,QAAM,CAAC,SAAS,UAAU,IAAIA,UAA6B,CAAC,CAAC;AAC7D,QAAM,CAAC,eAAe,gBAAgB,IAAIA,UAAS,KAAK;AACxD,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAuB,IAAI;AAIrD,QAAM,YAAYG,QAA2B,IAAI;AAMjD,QAAM,uBAAuBA,QAAO,CAAC;AAGrC,QAAM,iBAAiBA,QAAO,KAAK;AAInC,QAAM,mBAAmBD,aAAY,YAAY;AAC/C,QAAI;AACF,YAAM,EAAE,SAAS,YAAY,IAAI,MAAM,qBAAqB;AAC5D,iBAAW,WAAW;AAAA,IACxB,SAAS,KAAK;AACZ,cAAQ,MAAM,gCAAgC,GAAG;AACjD,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,6BAA6B,CAAC;AAAA,IAChF;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,gBAAgBA;AAAA,IACpB,OAAO,UAAmB,qBAA6C;AACrE,YAAM,aAAa,EAAE,qBAAqB;AAC1C,mBAAa,IAAI;AACjB,eAAS,IAAI;AAEb,UAAI;AAIF,YAAI,UAAU,SAAS;AACrB,oBAAU,QAAQ,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAC7D,oBAAU,UAAU;AACpB,oBAAU,IAAI;AAAA,QAChB;AAGA,cAAM,QAAsD;AAAA;AAAA,UAE1D,kBAAkB;AAAA,UAClB,kBAAkB;AAAA,UAClB,iBAAiB;AAAA,UACjB,SAAS;AAAA;AAAA;AAAA,UAET,GAAG;AAAA;AAAA,UAEH,GAAI,YAAY,EAAE,UAAU,EAAE,OAAO,SAAS,EAAE;AAAA,QAClD;AAEA,cAAM,cAAsC;AAAA,UAC1C;AAAA,UACA,OAAO;AAAA,QACT;AAEA,cAAM,YAAY,MAAM,UAAU,aAAa,aAAa,WAAW;AAKvE,YAAI,eAAe,qBAAqB,WAAW,eAAe,SAAS;AACzE,oBAAU,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AACrD;AAAA,QACF;AAEA,kBAAU,UAAU;AACpB,kBAAU,SAAS;AACnB,yBAAiB,IAAI;AAGrB,cAAM,iBAAiB;AAAA,MACzB,SAAS,KAAK;AACZ,gBAAQ,MAAM,gCAAgC,GAAG;AACjD,YAAI,eAAe,qBAAqB,SAAS;AAC/C,mBAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,6BAA6B,CAAC;AAC9E,2BAAiB,KAAK;AAAA,QACxB;AAAA,MACF,UAAE;AACA,YAAI,eAAe,qBAAqB,SAAS;AAC/C,uBAAa,KAAK;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAGA,QAAM,aAAaA,aAAY,MAAM;AAEnC,yBAAqB;AACrB,QAAI,UAAU,SAAS;AACrB,gBAAU,QAAQ,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAC7D,gBAAU,UAAU;AACpB,gBAAU,IAAI;AACd,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,EAAAD,WAAU,MAAM;AAId,mBAAe,UAAU;AAEzB,qBAAiB;AAGjB,cAAU,aAAa,iBAAiB,gBAAgB,gBAAgB;AAGxE,WAAO,MAAM;AACX,gBAAU,aAAa,oBAAoB,gBAAgB,gBAAgB;AAG3E,qBAAe,UAAU;AACzB,UAAI,UAAU,SAAS;AACrB,kBAAU,QAAQ,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAC7D,kBAAU,UAAU;AAAA,MACtB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,gBAAgB,CAAC;AAErB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;ACjJA,SAAS,aAAAG,YAAW,YAAAC,WAAU,UAAAC,SAAQ,eAAAC,oBAAmB;AACzD,SAAS,oBAAAC,yBAAwB;AACjC,SAAS,wBAAwB;AACjC,SAAS,6BAAgD;AAGzD,IAAM,aAAa;AA8EZ,SAAS,mBACd,QACA,UAAqC,CAAC,GACZ;AAC1B,QAAM,EAAE,aAAa,IAAI,eAAe,EAAE,IAAI;AAE9C,QAAM,CAAC,QAAQ,SAAS,IAAIH,UAAmB,MAAM,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AACpF,QAAM,CAAC,YAAY,aAAa,IAAIA,UAAmB,MAAM,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAC5F,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAmB,MAAM,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAC1F,QAAM,CAAC,YAAY,aAAa,IAAIA,UAAuB,IAAI;AAE/D,QAAM,iBAAiBC,QAAgC,IAAI;AAC3D,QAAM,YAAYA,QAA0C,IAAI;AAChE,QAAM,kBAAkBA,QAAiB,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAExE,QAAM,YAAYC;AAAA,IAChB,MAAM,cAAc,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAAA,IACnD,CAAC,YAAY;AAAA,EACf;AAEA,EAAAH,WAAU,MAAM;AACd,QAAI,CAAC,QAAQ;AACX,gBAAU,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AACzC,oBAAc,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAC7C,mBAAa,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAC5C,sBAAgB,UAAU,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC;AACxD;AAAA,IACF;AAEA,QAAI,YAAY;AAEhB,UAAM,kBAAkB,YAAY;AAClC,UAAI,CAAC,UAAW;AAEhB,YAAM,UAAUI,kBAAiB;AACjC,UAAI,QAAQ,UAAU,aAAa;AACjC,cAAM,QAAQ,OAAO;AAAA,MACvB;AACA,UAAI,CAAC,UAAW;AAGhB,YAAM,gBAAgB,OAAO,eAAe,EAAE,CAAC,GAAG,YAAY;AAC9D,YAAM,iBAAiB,eAAe,gBAAgB;AAKtD,YAAM,SAAS,QAAQ;AACvB,YAAM,sBAAsB,CAAC,QAAQ,OAAO,aAAa,UAAU,GAAG,CAAC;AACvE,UAAI,CAAC,UAAW;AAIhB,YAAM,cAAc,QAAQ,uBAAuB,mBAAmB;AAAA,QACpE,cAAc;AAAA,QACd,kBAAkB;AAAA,QAClB,kBAAkB;AAAA,UAChB,kBAAkB;AAAA,UAClB;AAAA,QACF;AAAA,MACF,CAAC;AACD,qBAAe,UAAU;AAEzB,kBAAY,mBAAmB,CAAC,UAAU;AACxC,gBAAQ,KAAK,0DAA0D,OAAO,KAAK,CAAC;AAAA,MACtF;AAIA,YAAM,SAAS,QAAQ,wBAAwB,MAAM;AACrD,gBAAU,UAAU;AACpB,aAAO,QAAQ,WAAW;AAE1B,sBAAgB,UAAU,IAAI,MAAM,cAAc,EAAE,KAAK,CAAC;AAG1D,kBAAY,KAAK,YAAY,CAAC,UAAwB;AACpD,YAAI,CAAC,UAAW;AAGhB,cAAM,OAAO,MAAM;AACnB,cAAM,kBAAkB,KAAK,SAAS;AACtC,cAAM,WAAW,gBAAgB;AAEjC,cAAM,aAAuB,CAAC;AAC9B,cAAM,YAAsB,CAAC;AAE7B,iBAAS,KAAK,GAAG,KAAK,iBAAiB,MAAM;AAC3C,mBAAS,EAAE,IAAI,KAAK,IAAI,KAAK,EAAE,IAAI,SAAS,EAAE,KAAK,KAAK,UAAU;AAClE,qBAAW,KAAK,iBAAiB,SAAS,EAAE,CAAC,CAAC;AAC9C,oBAAU,KAAK,iBAAiB,KAAK,kBAAkB,EAAE,CAAC,CAAC;AAAA,QAC7D;AAGA,cAAM,gBACJ,kBAAkB,eAAe,IAAI,MAAM,YAAY,EAAE,KAAK,WAAW,CAAC,CAAC,IAAI;AACjF,cAAM,cACJ,kBAAkB,eAAe,IAAI,MAAM,YAAY,EAAE,KAAK,UAAU,CAAC,CAAC,IAAI;AAEhF,kBAAU,aAAa;AACvB,qBAAa,WAAW;AACxB,sBAAc,CAAC,SAAS,cAAc,IAAI,CAAC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;AAAA,MACpF;AAIA,oBAAc,IAAI;AAAA,IACpB;AAEA,oBAAgB,EAAE,MAAM,CAAC,QAAQ;AAC/B,cAAQ,KAAK,8DAA8D,OAAO,GAAG,CAAC;AACtF,UAAI,WAAW;AACb,sBAAc,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,MACnE;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,kBAAY;AAEZ,UAAI,UAAU,SAAS;AACrB,YAAI;AACF,oBAAU,QAAQ,WAAW;AAAA,QAC/B,SAAS,KAAK;AACZ,kBAAQ,KAAK,6DAA6D,OAAO,GAAG,CAAC;AAAA,QACvF;AACA,kBAAU,UAAU;AAAA,MACtB;AAEA,UAAI,eAAe,SAAS;AAC1B,YAAI;AACF,yBAAe,QAAQ,WAAW;AAClC,yBAAe,QAAQ,KAAK,MAAM;AAAA,QACpC,SAAS,KAAK;AACZ,kBAAQ,KAAK,4DAA4D,OAAO,GAAG,CAAC;AAAA,QACtF;AACA,uBAAe,UAAU;AAAA,MAC3B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,YAAY,YAAY,CAAC;AAGrC,QAAM,QAAQ,iBAAiB,IAAK,OAAO,CAAC,KAAK,IAAK,KAAK,IAAI,GAAG,MAAM;AACxE,QAAM,YAAY,iBAAiB,IAAK,WAAW,CAAC,KAAK,IAAK,KAAK,IAAI,GAAG,UAAU;AAEpF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT;AACF;;;AC/OA,SAAS,YAAAC,WAAU,eAAAC,cAAa,aAAAC,YAAW,UAAAC,eAAc;AAKzD;AAAA,EAGE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA,oBAAAC;AAAA,OACK;AAgFA,SAAS,uBACd,QACA,WACA,iBACA,UAAsC,CAAC,GACT;AAC9B,QAAM,EAAE,cAAc,GAAG,kBAAkB,eAAe,GAAG,iBAAiB,IAAI;AAGlF,QAAM,CAAC,cAAc,eAAe,IAAIC,UAAS,KAAK;AACtD,QAAM,CAAC,gBAAgB,iBAAiB,IAAIA,UAAwB,IAAI;AACxE,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAuB,IAAI;AAG7D,QAAM,wBAAwBC,QAAO,CAAC;AAKtC,QAAM,qBAAqBA,QAAO,eAAe;AACjD,qBAAmB,UAAU;AAC7B,QAAM,iBAAiBA,QAAO,WAAW;AACzC,iBAAe,UAAU;AAIzB,QAAM,YAAYA,QAAO,MAAM;AAC/B,YAAU,UAAU;AAGpB,QAAM,EAAE,QAAQ,SAAS,eAAe,eAAe,OAAO,SAAS,IAAI,oBAAoB;AAG/F,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT,IAAI,mBAAmB,QAAQ;AAAA,IAC7B,cAAc,iBAAiB;AAAA,EACjC,CAAC;AAGD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT,IAAI,aAAa,QAAQ,gBAAgB;AAKzC,QAAM,iBAAiBC,aAAY,YAA8B;AAC/D,QAAI,CAAC,mBAAmB,SAAS;AAC/B;AAAA,QACE,IAAI,MAAM,4EAA4E;AAAA,MACxF;AACA,aAAO;AAAA,IACT;AAEA,QAAI;AACF,mBAAa,IAAI;AAEjB,UAAI,CAAC,cAAc;AACjB,cAAM,yBAAyB;AAC/B,wBAAgB,IAAI;AAAA,MACtB;AAKA,4BAAsB,UAAU,eAAe;AAE/C,aAAO,MAAM,SAAS;AAAA,IACxB,SAAS,KAAK;AACZ,mBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAChE,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,cAAc,QAAQ,CAAC;AAG3B,QAAM,gBAAgBA,aAAY,YAAY;AAC5C,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,QAAQ;AAAA,IACzB,SAAS,KAAK;AACZ,mBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAChE;AAAA,IACF;AAKA,UAAM,UAAU,mBAAmB;AACnC,UAAM,gBAAgB,UAAU;AAChC,QAAI,UAAU,SAAS;AACrB,YAAM,qBAAqB,cAAc,UAAU,CAAC,MAAM,EAAE,OAAO,OAAO;AAC1E,UAAI,uBAAuB,IAAI;AAC7B,cAAM,MAAM,IAAI;AAAA,UACd,kCAAkC,OAAO;AAAA,QAC3C;AACA,gBAAQ,KAAK,uBAAuB,IAAI,OAAO,EAAE;AACjD,qBAAa,GAAG;AAChB;AAAA,MACF;AAKA,YAAM,cAAc,KAAK,MAAM,sBAAsB,UAAU,OAAO,UAAU;AAMhF,YAAM,eAAe,sBAAsB;AAC3C,YAAM,gBAAgB,aAAa,iBAAiB;AACpD,YAAM,cAAcH,kBAAiB;AACrC,YAAM,YAAY,YAAY,aAAa;AAC3C,YAAM,uBAAuB,8BAA8B;AAAA,QACzD,iBAAiB;AAAA,QACjB;AAAA,QACA;AAAA,QACA,YAAY,OAAO;AAAA,MACrB,CAAC;AAGD,YAAM,oBAAoB,KAAK,IAAI,GAAG,OAAO,SAAS,oBAAoB;AAC1E,UAAI,sBAAsB,GAAG;AAC3B,gBAAQ;AAAA,UACN;AAAA,QACF;AACA,qBAAa,IAAI,MAAM,4DAA4D,CAAC;AACpF;AAAA,MACF;AAGA,YAAM,UAAqB;AAAA,QACzB,IAAI,QAAQ,KAAK,IAAI,CAAC;AAAA,QACtB,aAAa;AAAA,QACb;AAAA,QACA,iBAAiB;AAAA,QACjB,eAAe;AAAA,QACf,YAAY,OAAO;AAAA,QACnB,uBAAuB,OAAO;AAAA,QAC9B,MAAM;AAAA,QACN,MAAM,cAAa,oBAAI,KAAK,GAAE,mBAAmB,CAAC;AAAA,MACpD;AAIA,YAAM,YAAY,cAAc,IAAI,CAAC,OAAO,UAAU;AACpD,YAAI,UAAU,oBAAoB;AAChC,iBAAO;AAAA,YACL,GAAG;AAAA,YACH,OAAO;AAAA,cACL,GAAG,eAAe,MAAM,OAAO,aAAa,cAAc,iBAAiB;AAAA,cAC3E;AAAA,YACF;AAAA,UACF;AAAA,QACF;AACA,eAAO;AAAA,MACT,CAAC;AAED,gBAAU,SAAS;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,WAAW,SAAS,aAAa,CAAC;AAGtC,EAAAI,WAAU,MAAM;AACd,QAAI,CAAC,iBAAiB,QAAQ,WAAW,EAAG;AAE5C,QAAI,mBAAmB,MAAM;AAE3B,wBAAkB,QAAQ,CAAC,EAAE,QAAQ;AAAA,IACvC,WAAW,CAAC,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,cAAc,GAAG;AAE9D,YAAM,aAAa,QAAQ,CAAC,EAAE;AAC9B,wBAAkB,UAAU;AAC5B,gBAAU;AACV,oBAAc,YAAY,gBAAgB;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,eAAe,SAAS,gBAAgB,WAAW,eAAe,gBAAgB,CAAC;AAIvF,QAAM,iBAAiBF,QAAO,WAAW;AACzC,iBAAe,UAAU;AAGzB,QAAM,mBAAmBC,aAAY,YAAY;AAI/C,QAAI,eAAe,SAAS;AAC1B,mBAAa,IAAI,MAAM,+DAA+D,CAAC;AACvF;AAAA,IACF;AACA,QAAI;AACF,mBAAa,IAAI;AACjB,YAAM,cAAc,QAAW,gBAAgB;AAC/C,YAAM,yBAAyB;AAC/B,sBAAgB,IAAI;AAAA,IACtB,SAAS,KAAK;AACZ,mBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAClE;AAAA,EACF,GAAG,CAAC,eAAe,gBAAgB,CAAC;AAGpC,QAAM,eAAeA;AAAA,IACnB,OAAO,aAAqB;AAE1B,UAAI,eAAe,SAAS;AAC1B,qBAAa,IAAI,MAAM,uDAAuD,CAAC;AAC/E;AAAA,MACF;AACA,UAAI;AACF,qBAAa,IAAI;AACjB,0BAAkB,QAAQ;AAC1B,kBAAU;AACV,cAAM,cAAc,UAAU,gBAAgB;AAC9C,cAAM,yBAAyB;AAC/B,wBAAgB,IAAI;AAAA,MACtB,SAAS,KAAK;AACZ,qBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,MAClE;AAAA,IACF;AAAA,IACA,CAAC,eAAe,kBAAkB,SAAS;AAAA,EAC7C;AAEA,SAAO;AAAA;AAAA,IAEL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,aAAa,YAAY,cAAc;AAAA;AAAA,IAG9C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA,gBAAgB;AAAA,EAClB;AACF;","names":["error","useState","useEffect","useCallback","useRef","useEffect","useState","useRef","useCallback","getGlobalContext","useState","useCallback","useEffect","useRef","getGlobalContext","useState","useRef","useCallback","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveform-playlist/recording",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.0",
|
|
4
4
|
"description": "Audio recording support for waveform-playlist using AudioWorklet",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"vitest": "^3.2.6"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@waveform-playlist/core": "12.
|
|
51
|
-
"@waveform-playlist/
|
|
52
|
-
"@waveform-playlist/
|
|
53
|
-
"@waveform-playlist/
|
|
50
|
+
"@waveform-playlist/core": "12.6.0",
|
|
51
|
+
"@waveform-playlist/ui-components": "13.1.2",
|
|
52
|
+
"@waveform-playlist/playout": "12.5.1",
|
|
53
|
+
"@waveform-playlist/worklets": "13.0.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"react": "^18.0.0",
|