@waveform-playlist/recording 12.1.0 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -183,6 +183,15 @@ interface IntegratedRecordingOptions {
183
183
  * Default: 1024
184
184
  */
185
185
  samplesPerPixel?: number;
186
+ /**
187
+ * Latency offset to skip at the start of the recording, in **seconds**.
188
+ * Absolute replacement for the auto-computed `outputLatency + lookAhead` value
189
+ * — use this to apply an externally-measured round-trip latency. `0` disables
190
+ * compensation; when omitted, the auto-computed value is used. Pass the same
191
+ * value into the provider's `recordingState.latencyOffset` so the live preview
192
+ * matches the finalized clip. Negative or non-finite values are treated as 0.
193
+ */
194
+ latencyOffset?: number;
186
195
  }
187
196
  interface UseIntegratedRecordingReturn {
188
197
  isRecording: boolean;
package/dist/index.d.ts CHANGED
@@ -183,6 +183,15 @@ interface IntegratedRecordingOptions {
183
183
  * Default: 1024
184
184
  */
185
185
  samplesPerPixel?: number;
186
+ /**
187
+ * Latency offset to skip at the start of the recording, in **seconds**.
188
+ * Absolute replacement for the auto-computed `outputLatency + lookAhead` value
189
+ * — use this to apply an externally-measured round-trip latency. `0` disables
190
+ * compensation; when omitted, the auto-computed value is used. Pass the same
191
+ * value into the provider's `recordingState.latencyOffset` so the live preview
192
+ * matches the finalized clip. Negative or non-finite values are treated as 0.
193
+ */
194
+ latencyOffset?: number;
186
195
  }
187
196
  interface UseIntegratedRecordingReturn {
188
197
  isRecording: boolean;
package/dist/index.js CHANGED
@@ -521,7 +521,7 @@ var import_react4 = require("react");
521
521
  var import_core3 = require("@waveform-playlist/core");
522
522
  var import_playout3 = require("@waveform-playlist/playout");
523
523
  function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}) {
524
- const { currentTime = 0, audioConstraints, ...recordingOptions } = options;
524
+ const { currentTime = 0, audioConstraints, latencyOffset, ...recordingOptions } = options;
525
525
  const [isMonitoring, setIsMonitoring] = (0, import_react4.useState)(false);
526
526
  const [selectedDevice, setSelectedDevice] = (0, import_react4.useState)(null);
527
527
  const [hookError, setHookError] = (0, import_react4.useState)(null);
@@ -606,11 +606,12 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
606
606
  const outputLatency = audioContext.outputLatency ?? 0;
607
607
  const toneContext = (0, import_playout3.getGlobalContext)();
608
608
  const lookAhead = toneContext.lookAhead ?? 0;
609
- const latencyOffsetSamples = (0, import_core3.audibleLatencySamples)(
609
+ const latencyOffsetSamples = (0, import_core3.resolveRecordingOffsetSamples)({
610
+ overrideSeconds: latencyOffset,
610
611
  outputLatency,
611
612
  lookAhead,
612
- buffer.sampleRate
613
- );
613
+ sampleRate: buffer.sampleRate
614
+ });
614
615
  const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);
615
616
  if (effectiveDuration === 0) {
616
617
  console.warn(
@@ -641,7 +642,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
641
642
  });
642
643
  setTracks(newTracks);
643
644
  }
644
- }, [tracks, setTracks, stopRec]);
645
+ }, [tracks, setTracks, stopRec, latencyOffset]);
645
646
  (0, import_react4.useEffect)(() => {
646
647
  if (!hasPermission || devices.length === 0) return;
647
648
  if (selectedDevice === null) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport {\n useRecording,\n useMicrophoneAccess,\n useMicrophoneLevel,\n useIntegratedRecording,\n} from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n UseIntegratedRecordingReturn,\n IntegratedRecordingOptions,\n} from './hooks';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n","/**\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 { type ClipTrack, type AudioClip, audibleLatencySamples } 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\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, ...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:\n // Two sources of delay between recording start and audible playback:\n // 1. Tone.js lookAhead (~100ms) — Transport schedules audio ahead of real time\n // 2. Output latency — hardware DAC delay before audio reaches speakers\n // The user hears playback delayed by both, so they perform late relative\n // to the timeline. Skip that duration at the start of the recorded audio.\n // Shared formula with the live preview in PlaylistVisualization — keep both\n // paths using `audibleLatencySamples` so 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 = audibleLatencySamples(\n outputLatency,\n lookAhead,\n 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]);\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","import_react","import_react","import_playout","import_core","import_worklets","import_react","import_core","import_playout"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport {\n useRecording,\n useMicrophoneAccess,\n useMicrophoneLevel,\n useIntegratedRecording,\n} from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n UseIntegratedRecordingReturn,\n IntegratedRecordingOptions,\n} from './hooks';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n","/**\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","import_react","import_react","import_playout","import_core","import_worklets","import_react","import_core","import_playout"]}
package/dist/index.mjs CHANGED
@@ -489,14 +489,16 @@ function useMicrophoneLevel(stream, options = {}) {
489
489
 
490
490
  // src/hooks/useIntegratedRecording.ts
491
491
  import { useState as useState4, useCallback as useCallback4, useEffect as useEffect4, useRef as useRef3 } from "react";
492
- import { audibleLatencySamples } from "@waveform-playlist/core";
492
+ import {
493
+ resolveRecordingOffsetSamples
494
+ } from "@waveform-playlist/core";
493
495
  import {
494
496
  resumeGlobalAudioContext,
495
497
  getGlobalAudioContext,
496
498
  getGlobalContext as getGlobalContext3
497
499
  } from "@waveform-playlist/playout";
498
500
  function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}) {
499
- const { currentTime = 0, audioConstraints, ...recordingOptions } = options;
501
+ const { currentTime = 0, audioConstraints, latencyOffset, ...recordingOptions } = options;
500
502
  const [isMonitoring, setIsMonitoring] = useState4(false);
501
503
  const [selectedDevice, setSelectedDevice] = useState4(null);
502
504
  const [hookError, setHookError] = useState4(null);
@@ -581,11 +583,12 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
581
583
  const outputLatency = audioContext.outputLatency ?? 0;
582
584
  const toneContext = getGlobalContext3();
583
585
  const lookAhead = toneContext.lookAhead ?? 0;
584
- const latencyOffsetSamples = audibleLatencySamples(
586
+ const latencyOffsetSamples = resolveRecordingOffsetSamples({
587
+ overrideSeconds: latencyOffset,
585
588
  outputLatency,
586
589
  lookAhead,
587
- buffer.sampleRate
588
- );
590
+ sampleRate: buffer.sampleRate
591
+ });
589
592
  const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);
590
593
  if (effectiveDuration === 0) {
591
594
  console.warn(
@@ -616,7 +619,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
616
619
  });
617
620
  setTracks(newTracks);
618
621
  }
619
- }, [tracks, setTracks, stopRec]);
622
+ }, [tracks, setTracks, stopRec, latencyOffset]);
620
623
  useEffect4(() => {
621
624
  if (!hasPermission || devices.length === 0) return;
622
625
  if (selectedDevice === null) {
@@ -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 { type ClipTrack, type AudioClip, audibleLatencySamples } 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\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, ...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:\n // Two sources of delay between recording start and audible playback:\n // 1. Tone.js lookAhead (~100ms) — Transport schedules audio ahead of real time\n // 2. Output latency — hardware DAC delay before audio reaches speakers\n // The user hears playback delayed by both, so they perform late relative\n // to the timeline. Skip that duration at the start of the recorded audio.\n // Shared formula with the live preview in PlaylistVisualization — keep both\n // paths using `audibleLatencySamples` so 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 = audibleLatencySamples(\n outputLatency,\n lookAhead,\n 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]);\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\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waveform-playlist/recording",
3
- "version": "12.1.0",
3
+ "version": "12.2.0",
4
4
  "description": "Audio recording support for waveform-playlist using AudioWorklet",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -37,20 +37,20 @@
37
37
  ],
38
38
  "devDependencies": {
39
39
  "@testing-library/react": "^16.3.2",
40
- "@types/react": "^18.2.45",
41
- "@types/styled-components": "^5.1.26",
40
+ "@types/react": "^18.3.31",
41
+ "@types/styled-components": "^5.1.36",
42
42
  "jsdom": "^28.1.0",
43
43
  "react": "^18.3.1",
44
44
  "react-dom": "^18.3.1",
45
- "tsup": "^8.0.1",
46
- "typescript": "^5.3.3",
47
- "vitest": "^3.0.0"
45
+ "tsup": "^8.5.1",
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^3.2.6"
48
48
  },
49
49
  "dependencies": {
50
- "@waveform-playlist/core": "12.1.0",
50
+ "@waveform-playlist/core": "12.3.0",
51
+ "@waveform-playlist/playout": "12.4.0",
51
52
  "@waveform-playlist/worklets": "12.2.0",
52
- "@waveform-playlist/playout": "12.3.0",
53
- "@waveform-playlist/ui-components": "12.1.0"
53
+ "@waveform-playlist/ui-components": "13.0.1"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": "^18.0.0",