@waveform-playlist/recording 10.2.0 → 10.4.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
@@ -224,6 +224,11 @@ declare function useIntegratedRecording(tracks: ClipTrack[], setTracks: (tracks:
224
224
  * @returns Int8Array or Int16Array of peak values (min/max pairs)
225
225
  */
226
226
  declare function generatePeaks(samples: Float32Array, samplesPerPixel: number, bits?: 8 | 16): Int8Array | Int16Array;
227
+ /**
228
+ * Append new peaks to existing peaks array
229
+ * This is used for incremental peak updates during recording
230
+ */
231
+ declare function appendPeaks(existingPeaks: Int8Array | Int16Array, newSamples: Float32Array, samplesPerPixel: number, totalSamplesProcessed: number, bits?: 8 | 16): Int8Array | Int16Array;
227
232
 
228
233
  /**
229
234
  * Utility functions for working with AudioBuffers during recording
@@ -238,4 +243,4 @@ declare function concatenateAudioData(chunks: Float32Array[]): Float32Array;
238
243
  */
239
244
  declare function createAudioBuffer(audioContext: AudioContext, channelData: Float32Array[] | Float32Array, sampleRate: number, channelCount?: number): AudioBuffer;
240
245
 
241
- export { type IntegratedRecordingOptions, type MicrophoneDevice, type RecordingData, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, concatenateAudioData, createAudioBuffer, generatePeaks, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
246
+ export { type IntegratedRecordingOptions, type MicrophoneDevice, type RecordingData, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, appendPeaks, concatenateAudioData, createAudioBuffer, generatePeaks, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
package/dist/index.d.ts CHANGED
@@ -224,6 +224,11 @@ declare function useIntegratedRecording(tracks: ClipTrack[], setTracks: (tracks:
224
224
  * @returns Int8Array or Int16Array of peak values (min/max pairs)
225
225
  */
226
226
  declare function generatePeaks(samples: Float32Array, samplesPerPixel: number, bits?: 8 | 16): Int8Array | Int16Array;
227
+ /**
228
+ * Append new peaks to existing peaks array
229
+ * This is used for incremental peak updates during recording
230
+ */
231
+ declare function appendPeaks(existingPeaks: Int8Array | Int16Array, newSamples: Float32Array, samplesPerPixel: number, totalSamplesProcessed: number, bits?: 8 | 16): Int8Array | Int16Array;
227
232
 
228
233
  /**
229
234
  * Utility functions for working with AudioBuffers during recording
@@ -238,4 +243,4 @@ declare function concatenateAudioData(chunks: Float32Array[]): Float32Array;
238
243
  */
239
244
  declare function createAudioBuffer(audioContext: AudioContext, channelData: Float32Array[] | Float32Array, sampleRate: number, channelCount?: number): AudioBuffer;
240
245
 
241
- export { type IntegratedRecordingOptions, type MicrophoneDevice, type RecordingData, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, concatenateAudioData, createAudioBuffer, generatePeaks, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
246
+ export { type IntegratedRecordingOptions, type MicrophoneDevice, type RecordingData, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, appendPeaks, concatenateAudioData, createAudioBuffer, generatePeaks, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ appendPeaks: () => appendPeaks,
23
24
  concatenateAudioData: () => concatenateAudioData,
24
25
  createAudioBuffer: () => createAudioBuffer,
25
26
  generatePeaks: () => generatePeaks,
@@ -131,6 +132,9 @@ function useRecording(stream, options = {}) {
131
132
  const startTimeRef = (0, import_react.useRef)(0);
132
133
  const isRecordingRef = (0, import_react.useRef)(false);
133
134
  const isPausedRef = (0, import_react.useRef)(false);
135
+ const audioTrackRef = (0, import_react.useRef)(null);
136
+ const onTrackEndedRef = (0, import_react.useRef)(null);
137
+ const stopRecordingRef = (0, import_react.useRef)(null);
134
138
  const startDurationLoop = (0, import_react.useCallback)(() => {
135
139
  const tick = () => {
136
140
  if (isRecordingRef.current && !isPausedRef.current) {
@@ -157,6 +161,7 @@ function useRecording(stream, options = {}) {
157
161
  }
158
162
  }, []);
159
163
  const startRecording = (0, import_react.useCallback)(async () => {
164
+ if (isRecordingRef.current) return;
160
165
  if (!stream) {
161
166
  setError(new Error("No microphone stream available"));
162
167
  return;
@@ -221,11 +226,18 @@ function useRecording(stream, options = {}) {
221
226
  });
222
227
  };
223
228
  source.connect(workletNode);
224
- workletNode.port.postMessage({
225
- command: "start",
226
- sampleRate: context.sampleRate,
227
- channelCount: streamChannelCount
228
- });
229
+ workletNode.port.postMessage({ command: "start", channelCount: streamChannelCount });
230
+ const audioTrack = stream.getAudioTracks()[0] ?? null;
231
+ audioTrackRef.current = audioTrack;
232
+ if (audioTrack) {
233
+ const onEnded = () => {
234
+ console.warn("[waveform-playlist] Audio track ended (mic unplugged or revoked)");
235
+ stopRecordingRef.current?.();
236
+ setError(new Error("Microphone disconnected during recording"));
237
+ };
238
+ onTrackEndedRef.current = onEnded;
239
+ audioTrack.addEventListener("ended", onEnded);
240
+ }
229
241
  isRecordingRef.current = true;
230
242
  isPausedRef.current = false;
231
243
  setIsRecording(true);
@@ -238,10 +250,15 @@ function useRecording(stream, options = {}) {
238
250
  }
239
251
  }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);
240
252
  const stopRecording = (0, import_react.useCallback)(async () => {
241
- if (!isRecording) {
253
+ if (!isRecordingRef.current) {
242
254
  return null;
243
255
  }
244
256
  try {
257
+ if (audioTrackRef.current && onTrackEndedRef.current) {
258
+ audioTrackRef.current.removeEventListener("ended", onTrackEndedRef.current);
259
+ audioTrackRef.current = null;
260
+ onTrackEndedRef.current = null;
261
+ }
245
262
  if (workletNodeRef.current) {
246
263
  workletNodeRef.current.port.postMessage({ command: "stop" });
247
264
  if (mediaStreamSourceRef.current) {
@@ -285,7 +302,8 @@ function useRecording(stream, options = {}) {
285
302
  setError(err instanceof Error ? err : new Error("Failed to stop recording"));
286
303
  return null;
287
304
  }
288
- }, [isRecording, channelCount]);
305
+ }, [channelCount]);
306
+ stopRecordingRef.current = stopRecording;
289
307
  const pauseRecording = (0, import_react.useCallback)(() => {
290
308
  if (isRecording && !isPaused) {
291
309
  if (animationFrameRef.current !== null) {
@@ -306,6 +324,9 @@ function useRecording(stream, options = {}) {
306
324
  }, [isRecording, isPaused, duration, startDurationLoop]);
307
325
  (0, import_react.useEffect)(() => {
308
326
  return () => {
327
+ if (audioTrackRef.current && onTrackEndedRef.current) {
328
+ audioTrackRef.current.removeEventListener("ended", onTrackEndedRef.current);
329
+ }
309
330
  if (workletNodeRef.current) {
310
331
  workletNodeRef.current.port.postMessage({ command: "stop" });
311
332
  if (mediaStreamSourceRef.current) {
@@ -731,6 +752,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
731
752
  }
732
753
  // Annotate the CommonJS export names for ESM import in node:
733
754
  0 && (module.exports = {
755
+ appendPeaks,
734
756
  concatenateAudioData,
735
757
  createAudioBuffer,
736
758
  generatePeaks,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.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// Utilities\nexport { generatePeaks } from './utils/peaksGenerator';\nexport { createAudioBuffer, concatenateAudioData } from './utils/audioBufferUtils';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { recordingProcessorUrl } 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\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) {\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 // Load the worklet module directly on the raw AudioContext.\n // Tone.js's addAudioWorkletModule only loads ONE module per context\n // (caches _workletPromise). If meter-processor was loaded first by\n // useMicrophoneLevel, recording-processor is silently skipped.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(recordingProcessorUrl);\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 (!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 } = event.data as { channels: Float32Array[] };\n\n if (!channels || channels.length === 0) {\n console.warn('[waveform-playlist] Recording worklet sent empty or missing channels data');\n return;\n }\n\n // Accumulate per-channel samples\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 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 // 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 workletNode.port.postMessage({\n command: 'start',\n sampleRate: context.sampleRate,\n channelCount: streamChannelCount,\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 (!isRecording) {\n return null;\n }\n\n try {\n // Stop the worklet\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 stop:', String(err));\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\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 }\n }, [isRecording, channelCount]);\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\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 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\n useEffect(() => {\n return () => {\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 * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert channel data to AudioBuffer.\n * Accepts either per-channel Float32Array[] or a single Float32Array (mono, backwards compatible).\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n channelData: Float32Array[] | Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n // Backwards compatibility: single Float32Array → wrap as mono\n const channels: Float32Array[] =\n channelData instanceof Float32Array ? [channelData] : channelData;\n\n const length = channels[0]?.length ?? 0;\n const buffer = audioContext.createBuffer(channelCount, length, sampleRate);\n\n for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {\n buffer.copyToChannel(new Float32Array(channels[ch]), ch);\n }\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer (mono convenience)\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, [newSamples], sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, [combined], sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n // Clamp to valid range: Int16 is [-32768, 32767], Int8 is [-128, 127]\n peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\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 { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface 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 // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: 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 } 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 const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const totalLatency = outputLatency + lookAhead;\n const latencyOffsetSamples = Math.floor(totalLatency * buffer.sampleRate);\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":["newPeaks","result","error","import_react","import_react","import_playout","import_worklets","import_react","import_playout"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.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// Utilities\nexport { generatePeaks, appendPeaks } from './utils/peaksGenerator';\nexport { createAudioBuffer, concatenateAudioData } from './utils/audioBufferUtils';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { recordingProcessorUrl } 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\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) {\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 // Load the worklet module directly on the raw AudioContext.\n // Tone.js's addAudioWorkletModule only loads ONE module per context\n // (caches _workletPromise). If meter-processor was loaded first by\n // useMicrophoneLevel, recording-processor is silently skipped.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(recordingProcessorUrl);\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 } = event.data as { channels: Float32Array[] };\n\n if (!channels || channels.length === 0) {\n console.warn('[waveform-playlist] Recording worklet sent empty or missing channels data');\n return;\n }\n\n // Accumulate per-channel samples\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 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 // 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 // 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\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 stop:', String(err));\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\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 }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\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 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 * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert channel data to AudioBuffer.\n * Accepts either per-channel Float32Array[] or a single Float32Array (mono, backwards compatible).\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n channelData: Float32Array[] | Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n // Backwards compatibility: single Float32Array → wrap as mono\n const channels: Float32Array[] =\n channelData instanceof Float32Array ? [channelData] : channelData;\n\n const length = channels[0]?.length ?? 0;\n const buffer = audioContext.createBuffer(channelCount, length, sampleRate);\n\n for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {\n buffer.copyToChannel(new Float32Array(channels[ch]), ch);\n }\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer (mono convenience)\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, [newSamples], sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, [combined], sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n // Clamp to valid range: Int16 is [-32768, 32767], Int8 is [-128, 127]\n peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\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 { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface 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 // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: 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 } 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 const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const totalLatency = outputLatency + lookAhead;\n const latencyOffsetSamples = Math.floor(totalLatency * buffer.sampleRate);\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":["newPeaks","result","error","import_react","import_react","import_playout","import_worklets","import_react","import_playout"]}
package/dist/index.mjs CHANGED
@@ -99,6 +99,9 @@ function useRecording(stream, options = {}) {
99
99
  const startTimeRef = useRef(0);
100
100
  const isRecordingRef = useRef(false);
101
101
  const isPausedRef = useRef(false);
102
+ const audioTrackRef = useRef(null);
103
+ const onTrackEndedRef = useRef(null);
104
+ const stopRecordingRef = useRef(null);
102
105
  const startDurationLoop = useCallback(() => {
103
106
  const tick = () => {
104
107
  if (isRecordingRef.current && !isPausedRef.current) {
@@ -125,6 +128,7 @@ function useRecording(stream, options = {}) {
125
128
  }
126
129
  }, []);
127
130
  const startRecording = useCallback(async () => {
131
+ if (isRecordingRef.current) return;
128
132
  if (!stream) {
129
133
  setError(new Error("No microphone stream available"));
130
134
  return;
@@ -189,11 +193,18 @@ function useRecording(stream, options = {}) {
189
193
  });
190
194
  };
191
195
  source.connect(workletNode);
192
- workletNode.port.postMessage({
193
- command: "start",
194
- sampleRate: context.sampleRate,
195
- channelCount: streamChannelCount
196
- });
196
+ workletNode.port.postMessage({ command: "start", channelCount: streamChannelCount });
197
+ const audioTrack = stream.getAudioTracks()[0] ?? null;
198
+ audioTrackRef.current = audioTrack;
199
+ if (audioTrack) {
200
+ const onEnded = () => {
201
+ console.warn("[waveform-playlist] Audio track ended (mic unplugged or revoked)");
202
+ stopRecordingRef.current?.();
203
+ setError(new Error("Microphone disconnected during recording"));
204
+ };
205
+ onTrackEndedRef.current = onEnded;
206
+ audioTrack.addEventListener("ended", onEnded);
207
+ }
197
208
  isRecordingRef.current = true;
198
209
  isPausedRef.current = false;
199
210
  setIsRecording(true);
@@ -206,10 +217,15 @@ function useRecording(stream, options = {}) {
206
217
  }
207
218
  }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);
208
219
  const stopRecording = useCallback(async () => {
209
- if (!isRecording) {
220
+ if (!isRecordingRef.current) {
210
221
  return null;
211
222
  }
212
223
  try {
224
+ if (audioTrackRef.current && onTrackEndedRef.current) {
225
+ audioTrackRef.current.removeEventListener("ended", onTrackEndedRef.current);
226
+ audioTrackRef.current = null;
227
+ onTrackEndedRef.current = null;
228
+ }
213
229
  if (workletNodeRef.current) {
214
230
  workletNodeRef.current.port.postMessage({ command: "stop" });
215
231
  if (mediaStreamSourceRef.current) {
@@ -253,7 +269,8 @@ function useRecording(stream, options = {}) {
253
269
  setError(err instanceof Error ? err : new Error("Failed to stop recording"));
254
270
  return null;
255
271
  }
256
- }, [isRecording, channelCount]);
272
+ }, [channelCount]);
273
+ stopRecordingRef.current = stopRecording;
257
274
  const pauseRecording = useCallback(() => {
258
275
  if (isRecording && !isPaused) {
259
276
  if (animationFrameRef.current !== null) {
@@ -274,6 +291,9 @@ function useRecording(stream, options = {}) {
274
291
  }, [isRecording, isPaused, duration, startDurationLoop]);
275
292
  useEffect(() => {
276
293
  return () => {
294
+ if (audioTrackRef.current && onTrackEndedRef.current) {
295
+ audioTrackRef.current.removeEventListener("ended", onTrackEndedRef.current);
296
+ }
277
297
  if (workletNodeRef.current) {
278
298
  workletNodeRef.current.port.postMessage({ command: "stop" });
279
299
  if (mediaStreamSourceRef.current) {
@@ -702,6 +722,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
702
722
  };
703
723
  }
704
724
  export {
725
+ appendPeaks,
705
726
  concatenateAudioData,
706
727
  createAudioBuffer,
707
728
  generatePeaks,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.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 } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { recordingProcessorUrl } 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\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) {\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 // Load the worklet module directly on the raw AudioContext.\n // Tone.js's addAudioWorkletModule only loads ONE module per context\n // (caches _workletPromise). If meter-processor was loaded first by\n // useMicrophoneLevel, recording-processor is silently skipped.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(recordingProcessorUrl);\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 (!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 } = event.data as { channels: Float32Array[] };\n\n if (!channels || channels.length === 0) {\n console.warn('[waveform-playlist] Recording worklet sent empty or missing channels data');\n return;\n }\n\n // Accumulate per-channel samples\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 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 // 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 workletNode.port.postMessage({\n command: 'start',\n sampleRate: context.sampleRate,\n channelCount: streamChannelCount,\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 (!isRecording) {\n return null;\n }\n\n try {\n // Stop the worklet\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 stop:', String(err));\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\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 }\n }, [isRecording, channelCount]);\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\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 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\n useEffect(() => {\n return () => {\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 * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert channel data to AudioBuffer.\n * Accepts either per-channel Float32Array[] or a single Float32Array (mono, backwards compatible).\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n channelData: Float32Array[] | Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n // Backwards compatibility: single Float32Array → wrap as mono\n const channels: Float32Array[] =\n channelData instanceof Float32Array ? [channelData] : channelData;\n\n const length = channels[0]?.length ?? 0;\n const buffer = audioContext.createBuffer(channelCount, length, sampleRate);\n\n for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {\n buffer.copyToChannel(new Float32Array(channels[ch]), ch);\n }\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer (mono convenience)\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, [newSamples], sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, [combined], sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n // Clamp to valid range: Int16 is [-32768, 32767], Int8 is [-128, 127]\n peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\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 { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface 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 // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: 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 } 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 const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const totalLatency = outputLatency + lookAhead;\n const latencyOffsetSamples = Math.floor(totalLatency * buffer.sampleRate);\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":["newPeaks","result","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/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.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 } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { recordingProcessorUrl } 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\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) {\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 // Load the worklet module directly on the raw AudioContext.\n // Tone.js's addAudioWorkletModule only loads ONE module per context\n // (caches _workletPromise). If meter-processor was loaded first by\n // useMicrophoneLevel, recording-processor is silently skipped.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(recordingProcessorUrl);\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 } = event.data as { channels: Float32Array[] };\n\n if (!channels || channels.length === 0) {\n console.warn('[waveform-playlist] Recording worklet sent empty or missing channels data');\n return;\n }\n\n // Accumulate per-channel samples\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 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 // 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 // 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\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 stop:', String(err));\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\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 }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\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 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 * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert channel data to AudioBuffer.\n * Accepts either per-channel Float32Array[] or a single Float32Array (mono, backwards compatible).\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n channelData: Float32Array[] | Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n // Backwards compatibility: single Float32Array → wrap as mono\n const channels: Float32Array[] =\n channelData instanceof Float32Array ? [channelData] : channelData;\n\n const length = channels[0]?.length ?? 0;\n const buffer = audioContext.createBuffer(channelCount, length, sampleRate);\n\n for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {\n buffer.copyToChannel(new Float32Array(channels[ch]), ch);\n }\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer (mono convenience)\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, [newSamples], sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, [combined], sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n // Clamp to valid range: Int16 is [-32768, 32767], Int8 is [-128, 127]\n peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\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 { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface 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 // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: 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 } 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 const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const totalLatency = outputLatency + lookAhead;\n const latencyOffsetSamples = Math.floor(totalLatency * buffer.sampleRate);\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":["newPeaks","result","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": "10.2.0",
3
+ "version": "10.4.0",
4
4
  "description": "Audio recording support for waveform-playlist using AudioWorklet",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -43,10 +43,10 @@
43
43
  "vitest": "^3.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@waveform-playlist/playout": "10.2.0",
47
- "@waveform-playlist/core": "10.2.0",
48
- "@waveform-playlist/ui-components": "10.2.0",
49
- "@waveform-playlist/worklets": "10.2.0"
46
+ "@waveform-playlist/core": "10.4.0",
47
+ "@waveform-playlist/playout": "10.4.0",
48
+ "@waveform-playlist/ui-components": "10.4.0",
49
+ "@waveform-playlist/worklets": "10.4.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": "^18.0.0",