@waveform-playlist/recording 9.4.1 → 9.5.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.mjs CHANGED
@@ -12,10 +12,13 @@ function concatenateAudioData(chunks) {
12
12
  }
13
13
  return result;
14
14
  }
15
- function createAudioBuffer(audioContext, samples, sampleRate, channelCount = 1) {
16
- const buffer = audioContext.createBuffer(channelCount, samples.length, sampleRate);
17
- const typedSamples = new Float32Array(samples);
18
- buffer.copyToChannel(typedSamples, 0);
15
+ function createAudioBuffer(audioContext, channelData, sampleRate, channelCount = 1) {
16
+ const channels = channelData instanceof Float32Array ? [channelData] : channelData;
17
+ const length = channels[0]?.length ?? 0;
18
+ const buffer = audioContext.createBuffer(channelCount, length, sampleRate);
19
+ for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {
20
+ buffer.copyToChannel(new Float32Array(channels[ch]), ch);
21
+ }
19
22
  return buffer;
20
23
  }
21
24
 
@@ -73,17 +76,19 @@ function appendPeaks(existingPeaks, newSamples, samplesPerPixel, totalSamplesPro
73
76
 
74
77
  // src/hooks/useRecording.ts
75
78
  import { getContext } from "tone";
79
+ function emptyPeaks(bits) {
80
+ return bits === 8 ? new Int8Array(0) : new Int16Array(0);
81
+ }
76
82
  function useRecording(stream, options = {}) {
77
- const { channelCount = 1, samplesPerPixel = 1024 } = options;
83
+ const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;
78
84
  const [isRecording, setIsRecording] = useState(false);
79
85
  const [isPaused, setIsPaused] = useState(false);
80
86
  const [duration, setDuration] = useState(0);
81
- const [peaks, setPeaks] = useState(new Int16Array(0));
87
+ const [peaks, setPeaks] = useState([emptyPeaks(bits)]);
82
88
  const [audioBuffer, setAudioBuffer] = useState(null);
83
89
  const [error, setError] = useState(null);
84
90
  const [level, setLevel] = useState(0);
85
91
  const [peakLevel, setPeakLevel] = useState(0);
86
- const bits = 16;
87
92
  const workletLoadedRef = useRef(false);
88
93
  const workletNodeRef = useRef(null);
89
94
  const mediaStreamSourceRef = useRef(null);
@@ -119,36 +124,60 @@ function useRecording(stream, options = {}) {
119
124
  await context.resume();
120
125
  }
121
126
  await loadWorklet();
127
+ const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;
128
+ if (detectedChannelCount === void 0) {
129
+ console.warn(
130
+ `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`
131
+ );
132
+ }
133
+ const streamChannelCount = detectedChannelCount ?? channelCount;
122
134
  const source = context.createMediaStreamSource(stream);
123
135
  mediaStreamSourceRef.current = source;
124
- const workletNode = context.createAudioWorkletNode("recording-processor");
136
+ const workletNode = context.createAudioWorkletNode("recording-processor", {
137
+ channelCount: streamChannelCount,
138
+ channelCountMode: "explicit"
139
+ });
125
140
  workletNodeRef.current = workletNode;
126
- source.connect(workletNode);
141
+ recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);
142
+ totalSamplesRef.current = 0;
143
+ setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));
144
+ setAudioBuffer(null);
145
+ setLevel(0);
146
+ setPeakLevel(0);
127
147
  workletNode.port.onmessage = (event) => {
128
- const { samples } = event.data;
129
- recordedChunksRef.current.push(samples);
130
- totalSamplesRef.current += samples.length;
131
- setPeaks(
132
- (prevPeaks) => appendPeaks(
133
- prevPeaks,
134
- samples,
135
- samplesPerPixel,
136
- totalSamplesRef.current - samples.length,
137
- bits
138
- )
139
- );
148
+ const { channels } = event.data;
149
+ if (!channels || channels.length === 0) {
150
+ console.warn("[waveform-playlist] Recording worklet sent empty or missing channels data");
151
+ return;
152
+ }
153
+ for (let ch = 0; ch < channels.length; ch++) {
154
+ if (!recordedChunksRef.current[ch]) {
155
+ console.warn(
156
+ `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`
157
+ );
158
+ recordedChunksRef.current[ch] = [];
159
+ }
160
+ recordedChunksRef.current[ch].push(channels[ch]);
161
+ }
162
+ const samplesProcessedBefore = totalSamplesRef.current;
163
+ totalSamplesRef.current += channels[0].length;
164
+ setPeaks((prevPeaks) => {
165
+ const updated = [];
166
+ for (let ch = 0; ch < channels.length; ch++) {
167
+ const prev = prevPeaks[ch] ?? emptyPeaks(bits);
168
+ updated.push(
169
+ appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)
170
+ );
171
+ }
172
+ return updated;
173
+ });
140
174
  };
175
+ source.connect(workletNode);
141
176
  workletNode.port.postMessage({
142
177
  command: "start",
143
178
  sampleRate: context.sampleRate,
144
- channelCount
179
+ channelCount: streamChannelCount
145
180
  });
146
- recordedChunksRef.current = [];
147
- totalSamplesRef.current = 0;
148
- setPeaks(new Int16Array(0));
149
- setAudioBuffer(null);
150
- setLevel(0);
151
- setPeakLevel(0);
152
181
  isRecordingRef.current = true;
153
182
  isPausedRef.current = false;
154
183
  setIsRecording(true);
@@ -166,7 +195,7 @@ function useRecording(stream, options = {}) {
166
195
  console.error("Failed to start recording:", err);
167
196
  setError(err instanceof Error ? err : new Error("Failed to start recording"));
168
197
  }
169
- }, [stream, channelCount, samplesPerPixel, loadWorklet]);
198
+ }, [stream, channelCount, samplesPerPixel, bits, loadWorklet]);
170
199
  const stopRecording = useCallback(async () => {
171
200
  if (!isRecording) {
172
201
  return null;
@@ -177,7 +206,8 @@ function useRecording(stream, options = {}) {
177
206
  if (mediaStreamSourceRef.current) {
178
207
  try {
179
208
  mediaStreamSourceRef.current.disconnect(workletNodeRef.current);
180
- } catch {
209
+ } catch (err) {
210
+ console.warn("[waveform-playlist] Source disconnect during stop:", String(err));
181
211
  }
182
212
  }
183
213
  workletNodeRef.current.disconnect();
@@ -186,10 +216,11 @@ function useRecording(stream, options = {}) {
186
216
  cancelAnimationFrame(animationFrameRef.current);
187
217
  animationFrameRef.current = null;
188
218
  }
189
- const allSamples = concatenateAudioData(recordedChunksRef.current);
190
219
  const context = getContext();
191
220
  const rawContext = context.rawContext;
192
- const buffer = createAudioBuffer(rawContext, allSamples, rawContext.sampleRate, channelCount);
221
+ const numChannels = recordedChunksRef.current.length || channelCount;
222
+ const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));
223
+ const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);
193
224
  setAudioBuffer(buffer);
194
225
  setDuration(buffer.duration);
195
226
  isRecordingRef.current = false;
@@ -236,7 +267,8 @@ function useRecording(stream, options = {}) {
236
267
  if (mediaStreamSourceRef.current) {
237
268
  try {
238
269
  mediaStreamSourceRef.current.disconnect(workletNodeRef.current);
239
- } catch {
270
+ } catch (err) {
271
+ console.warn("[waveform-playlist] Source disconnect during cleanup:", String(err));
240
272
  }
241
273
  }
242
274
  workletNodeRef.current.disconnect();
@@ -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","../src/components/RecordButton.tsx","../src/components/MicrophoneSelector.tsx","../src/components/RecordingIndicator.tsx","../src/components/VUMeter.tsx"],"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 { getContext } from 'tone';\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [peaks, setPeaks] = useState<Int8Array | Int16Array>(new Int16Array(0));\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 const bits: 8 | 16 = 16; // Match the bit depth used by the final waveform\n\n // Global flag to prevent loading worklet multiple times\n // (AudioWorklet processors can only be registered once per AudioContext)\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 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 // 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 = getContext();\n // Load the worklet module\n // Use a relative path that works when bundled\n const workletUrl = new URL('./worklet/recording-processor.worklet.js', import.meta.url).href;\n\n // Use Tone's addAudioWorkletModule for cross-browser compatibility\n await context.addAudioWorkletModule(workletUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.error('Failed to load AudioWorklet module:', err);\n throw new Error('Failed to load recording processor');\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 = getContext();\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 // Create AudioWorklet node using Tone's method\n const workletNode = context.createAudioWorkletNode('recording-processor');\n workletNodeRef.current = workletNode;\n\n // Connect source to worklet (but not to destination - no monitoring)\n source.connect(workletNode);\n\n //Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { samples } = event.data;\n\n // Accumulate samples\n recordedChunksRef.current.push(samples);\n totalSamplesRef.current += samples.length;\n\n // Update peaks incrementally for live waveform visualization\n setPeaks((prevPeaks) =>\n appendPeaks(\n prevPeaks,\n samples,\n samplesPerPixel,\n totalSamplesRef.current - samples.length,\n bits\n )\n );\n\n // Note: VU meter levels come from useMicrophoneLevel (AnalyserNode)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Start the worklet processor\n workletNode.port.postMessage({\n command: 'start',\n sampleRate: context.sampleRate,\n channelCount,\n });\n\n // Reset state\n recordedChunksRef.current = [];\n totalSamplesRef.current = 0;\n setPeaks(new Int16Array(0));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n\n // Start duration update loop\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n } catch (err) {\n console.error('Failed to start recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, loadWorklet]);\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 {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\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 chunks\n const allSamples = concatenateAudioData(recordedChunksRef.current);\n const context = getContext();\n // Use rawContext for createBuffer (native AudioContext method)\n const rawContext = context.rawContext as AudioContext;\n const buffer = createAudioBuffer(rawContext, allSamples, rawContext.sampleRate, channelCount);\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.error('Failed to stop recording:', 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\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n }\n }, [isRecording, isPaused, duration]);\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 {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\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 Float32Array to AudioBuffer\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n samples: Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n const buffer = audioContext.createBuffer(channelCount, samples.length, sampleRate);\n\n // Copy samples to buffer (for now, just mono)\n // Create a new Float32Array to ensure correct type\n const typedSamples = new Float32Array(samples);\n buffer.copyToChannel(typedSamples, 0);\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer\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 and enumerate devices\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Cleanup on unmount\n return () => {\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 Tone.js Meter for real-time audio level monitoring.\n */\n\nimport { useEffect, useState, useRef } from 'react';\nimport { Meter, getContext, connect } from 'tone';\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 * FFT size for the analyser\n * Default: 256\n */\n fftSize?: number;\n\n /**\n * Smoothing time constant (0-1)\n * Higher values = smoother but slower response\n * Default: 0.8\n */\n smoothingTimeConstant?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current audio level (0-1)\n * 0 = silence, 1 = maximum level\n */\n level: number;\n\n /**\n * Peak level since last reset (0-1)\n */\n peakLevel: number;\n\n /**\n * Reset the peak level\n */\n resetPeak: () => void;\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 level and peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { level, peakLevel, resetPeak } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, smoothingTimeConstant = 0.8 } = options;\n\n const [level, setLevel] = useState(0);\n const [peakLevel, setPeakLevel] = useState(0);\n\n const meterRef = useRef<Meter | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const animationFrameRef = useRef<number | null>(null);\n\n const resetPeak = () => setPeakLevel(0);\n\n useEffect(() => {\n if (!stream) {\n setLevel(0);\n setPeakLevel(0);\n return;\n }\n\n let isMounted = true;\n\n // Setup audio monitoring\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n // Get Tone's context and resume if needed\n const context = getContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n if (!isMounted) return;\n\n // Create Tone.js Meter for level monitoring\n // Pass context to ensure it's created in the same context as the source\n const meter = new Meter({ smoothing: smoothingTimeConstant, context });\n meterRef.current = meter;\n\n // Create MediaStreamSource from the SAME context as the meter\n // Note: This creates a separate source from useRecording, but that's OK\n // since we're only using it for level monitoring (not recording)\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n\n // Connect source to meter using Tone's connect function\n connect(source, meter);\n\n // Start level monitoring\n const updateInterval = 1000 / updateRate;\n let lastUpdateTime = 0;\n\n const updateLevel = (timestamp: number) => {\n if (!isMounted || !meterRef.current) return;\n\n if (timestamp - lastUpdateTime >= updateInterval) {\n lastUpdateTime = timestamp;\n\n // Meter.getValue() returns dB, convert to 0-1 range\n const db = meterRef.current.getValue();\n const dbValue = typeof db === 'number' ? db : db[0];\n // dB is typically -Infinity to 0, map -100dB..0dB to 0..1\n // Using -100dB as floor since Firefox seems to report lower values\n const normalized = Math.max(0, Math.min(1, (dbValue + 100) / 100));\n\n setLevel(normalized);\n setPeakLevel((prev) => Math.max(prev, normalized));\n }\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n setupMonitoring();\n\n // Cleanup\n return () => {\n isMounted = false;\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Disconnect and clean up\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch {\n // Ignore disconnect errors\n }\n sourceRef.current = null;\n }\n\n if (meterRef.current) {\n meterRef.current.dispose();\n meterRef.current = null;\n }\n };\n }, [stream, smoothingTimeConstant, updateRate]);\n\n return {\n level,\n peakLevel,\n resetPeak,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect } 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 { resumeGlobalAudioContext } 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 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 // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const { level, peakLevel } = useMicrophoneLevel(stream);\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 const startRecording = useCallback(async () => {\n if (!selectedTrackId) {\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 await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [selectedTrackId, 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 if (buffer && selectedTrackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === selectedTrackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${selectedTrackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.error(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Calculate start position: max(currentTime, lastClipEndTime)\n const currentTimeSamples = Math.floor(currentTime * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n // Find the end time of the last clip (in samples)\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n // Use whichever is greater: cursor position or last clip end\n const startSample = Math.max(currentTimeSamples, lastClipEndSample);\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: buffer.length,\n offsetSamples: 0,\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 }, [selectedTrackId, tracks, setTracks, currentTime, stopRec]);\n\n // Auto-select the first device when devices become available\n useEffect(() => {\n // Only auto-select if we have permission, devices are available, and nothing is selected yet\n if (hasPermission && devices.length > 0 && selectedDevice === null) {\n setSelectedDevice(devices[0].deviceId);\n }\n }, [hasPermission, devices, selectedDevice]);\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 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]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n error: hookError || micError || 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","/**\n * RecordButton - Control button for starting/stopping recording\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordButtonProps {\n isRecording: boolean;\n onClick: () => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Button = styled.button<{ $isRecording: boolean }>`\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border: none;\n border-radius: 0.25rem;\n cursor: pointer;\n transition: all 0.2s ease-in-out;\n background: ${(props) => (props.$isRecording ? '#dc3545' : '#e74c3c')};\n color: white;\n\n &:hover:not(:disabled) {\n background: ${(props) => (props.$isRecording ? '#c82333' : '#c0392b')};\n transform: translateY(-1px);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n &:focus {\n outline: none;\n box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.3);\n }\n`;\n\nconst RecordingIndicator = styled.span`\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: white;\n margin-right: 0.5rem;\n animation: pulse 1.5s ease-in-out infinite;\n\n @keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n`;\n\nexport const RecordButton: React.FC<RecordButtonProps> = ({\n isRecording,\n onClick,\n disabled = false,\n className,\n}) => {\n return (\n <Button\n $isRecording={isRecording}\n onClick={onClick}\n disabled={disabled}\n className={className}\n aria-label={isRecording ? 'Stop recording' : 'Start recording'}\n >\n {isRecording && <RecordingIndicator />}\n {isRecording ? 'Stop Recording' : 'Record'}\n </Button>\n );\n};\n","/**\n * MicrophoneSelector - Dropdown for selecting microphone input device\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\nimport { BaseSelect, BaseLabel } from '@waveform-playlist/ui-components';\nimport { MicrophoneDevice } from '../types';\n\nexport interface MicrophoneSelectorProps {\n devices: MicrophoneDevice[];\n selectedDeviceId?: string;\n onDeviceChange: (deviceId: string) => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Select = styled(BaseSelect)`\n min-width: 200px;\n`;\n\nconst Label = styled(BaseLabel)`\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n`;\n\nexport const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = ({\n devices,\n selectedDeviceId,\n onDeviceChange,\n disabled = false,\n className,\n}) => {\n const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {\n onDeviceChange(event.target.value);\n };\n\n // Use first device if no selection provided\n const currentValue = selectedDeviceId || (devices.length > 0 ? devices[0].deviceId : '');\n\n return (\n <Label className={className}>\n Microphone\n <Select\n value={currentValue}\n onChange={handleChange}\n disabled={disabled || devices.length === 0}\n >\n {devices.length === 0 ? (\n <option value=\"\">No microphones found</option>\n ) : (\n devices.map((device) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))\n )}\n </Select>\n </Label>\n );\n};\n","/**\n * RecordingIndicator - Shows recording status, duration, and visual indicator\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordingIndicatorProps {\n isRecording: boolean;\n isPaused?: boolean;\n duration: number; // in seconds\n formatTime?: (seconds: number) => string;\n className?: string;\n}\n\nconst Container = styled.div<{ $isRecording: boolean }>`\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.5rem 0.75rem;\n background: ${(props) => (props.$isRecording ? '#fff3cd' : 'transparent')};\n border-radius: 0.25rem;\n transition: background 0.2s ease-in-out;\n`;\n\nconst Dot = styled.div<{ $isRecording: boolean; $isPaused: boolean }>`\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n opacity: ${(props) => (props.$isRecording ? 1 : 0)};\n transition: opacity 0.2s ease-in-out;\n\n ${(props) =>\n props.$isRecording &&\n !props.$isPaused &&\n `\n animation: blink 1.5s ease-in-out infinite;\n\n @keyframes blink {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n `}\n`;\n\nconst Duration = styled.span`\n font-family: 'Courier New', Monaco, monospace;\n font-size: 1rem;\n font-weight: 600;\n color: #495057;\n min-width: 70px;\n`;\n\nconst Status = styled.span<{ $isPaused: boolean }>`\n font-size: 0.75rem;\n font-weight: 500;\n color: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n text-transform: uppercase;\n`;\n\nconst defaultFormatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n};\n\nexport const RecordingIndicator: React.FC<RecordingIndicatorProps> = ({\n isRecording,\n isPaused = false,\n duration,\n formatTime = defaultFormatTime,\n className,\n}) => {\n return (\n <Container $isRecording={isRecording} className={className}>\n <Dot $isRecording={isRecording} $isPaused={isPaused} />\n <Duration>{formatTime(duration)}</Duration>\n {isRecording && <Status $isPaused={isPaused}>{isPaused ? 'Paused' : 'Recording'}</Status>}\n </Container>\n );\n};\n","/**\n * VU Meter Component\n *\n * Displays real-time audio input levels with color-coded zones\n * and peak indicator.\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface VUMeterProps {\n /**\n * Current audio level (0-1)\n */\n level: number;\n\n /**\n * Peak level (0-1)\n * Optional - if provided, shows peak indicator\n */\n peakLevel?: number;\n\n /**\n * Width of the meter in pixels\n * Default: 200\n */\n width?: number;\n\n /**\n * Height of the meter in pixels\n * Default: 20\n */\n height?: number;\n\n /**\n * Additional CSS class name\n */\n className?: string;\n}\n\nconst MeterContainer = styled.div<{ $width: number; $height: number }>`\n position: relative;\n width: ${(props) => props.$width}px;\n height: ${(props) => props.$height}px;\n background: #2c3e50;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);\n`;\n\n// Helper to get gradient color based on level\nconst getLevelGradient = (level: number): string => {\n if (level < 0.6) return 'linear-gradient(90deg, #27ae60, #2ecc71)';\n if (level < 0.85) return 'linear-gradient(90deg, #f39c12, #f1c40f)';\n return 'linear-gradient(90deg, #c0392b, #e74c3c)';\n};\n\n// Use .attrs() for frequently changing styles to avoid generating new CSS classes\nconst MeterFill = styled.div.attrs<{ $level: number; $height: number }>((props) => ({\n style: {\n width: `${props.$level * 100}%`,\n height: `${props.$height}px`,\n background: getLevelGradient(props.$level),\n boxShadow: props.$level > 0.01 ? '0 0 8px rgba(255, 255, 255, 0.3)' : 'none',\n },\n}))<{ $level: number; $height: number }>`\n position: absolute;\n left: 0;\n top: 0;\n transition:\n width 0.05s ease-out,\n background 0.1s ease-out;\n`;\n\n// Use .attrs() for frequently changing left position\nconst PeakIndicator = styled.div.attrs<{ $peakLevel: number; $height: number }>((props) => ({\n style: {\n left: `${props.$peakLevel * 100}%`,\n height: `${props.$height}px`,\n },\n}))<{ $peakLevel: number; $height: number }>`\n position: absolute;\n top: 0;\n width: 2px;\n background: #ecf0f1;\n box-shadow: 0 0 4px rgba(236, 240, 241, 0.8);\n transition: left 0.1s ease-out;\n`;\n\nconst ScaleMarkers = styled.div<{ $height: number }>`\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: ${(props) => props.$height}px;\n pointer-events: none;\n`;\n\nconst ScaleMark = styled.div<{ $position: number; $height: number }>`\n position: absolute;\n left: ${(props) => props.$position}%;\n top: 0;\n width: 1px;\n height: ${(props) => props.$height}px;\n background: rgba(255, 255, 255, 0.2);\n`;\n\n/**\n * VU Meter component for displaying audio input levels\n *\n * @example\n * ```typescript\n * import { useMicrophoneLevel, VUMeter } from '@waveform-playlist/recording';\n *\n * const { level, peakLevel } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} width={300} height={24} />;\n * ```\n */\nconst VUMeterComponent: React.FC<VUMeterProps> = ({\n level,\n peakLevel,\n width = 200,\n height = 20,\n className,\n}) => {\n // Clamp values to 0-1 range\n const clampedLevel = Math.max(0, Math.min(1, level));\n const clampedPeak = peakLevel !== undefined ? Math.max(0, Math.min(1, peakLevel)) : 0;\n\n return (\n <MeterContainer $width={width} $height={height} className={className}>\n <MeterFill $level={clampedLevel} $height={height} />\n\n {peakLevel !== undefined && clampedPeak > 0 && (\n <PeakIndicator $peakLevel={clampedPeak} $height={height} />\n )}\n\n <ScaleMarkers $height={height}>\n <ScaleMark $position={60} $height={height} />\n <ScaleMark $position={85} $height={height} />\n </ScaleMarkers>\n </MeterContainer>\n );\n};\n\n// Memoize to prevent unnecessary re-renders when parent updates\nexport const VUMeter = React.memo(VUMeterComponent);\n"],"mappings":"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","names":["newPeaks","result","useState","useEffect","useCallback","useEffect","useState","useRef","getContext","useState","useCallback","useEffect","useState","useCallback","useEffect","styled","jsx","jsxs","styled","jsx","jsxs","RecordingIndicator","styled","jsx","jsxs"]}
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","../src/components/RecordButton.tsx","../src/components/MicrophoneSelector.tsx","../src/components/RecordingIndicator.tsx","../src/components/VUMeter.tsx"],"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 { getContext } from 'tone';\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 // Global flag to prevent loading worklet multiple times\n // (AudioWorklet processors can only be registered once per AudioContext)\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 // 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 = getContext();\n // Load the worklet module\n // Use a relative path that works when bundled\n const workletUrl = new URL('./worklet/recording-processor.worklet.js', import.meta.url).href;\n\n // Use Tone's addAudioWorkletModule for cross-browser compatibility\n await context.addAudioWorkletModule(workletUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.error('Failed to load AudioWorklet module:', err);\n throw new Error('Failed to load recording processor');\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 = getContext();\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 // Detect actual channel count from the stream's audio track settings.\n // Falls back to the user-provided channelCount option.\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 // 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 // Create AudioWorklet node — set channelCount to match the stream\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit',\n });\n workletNodeRef.current = workletNode;\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 (AnalyserNode)\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\n // Start duration update loop\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n } catch (err) {\n console.error('Failed to start recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet]);\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 = getContext();\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 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.error('Failed to stop recording:', 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\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n }\n }, [isRecording, isPaused, duration]);\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 workletNodeRef.current.disconnect();\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 and enumerate devices\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Cleanup on unmount\n return () => {\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 Tone.js Meter for real-time audio level monitoring.\n */\n\nimport { useEffect, useState, useRef } from 'react';\nimport { Meter, getContext, connect } from 'tone';\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 * FFT size for the analyser\n * Default: 256\n */\n fftSize?: number;\n\n /**\n * Smoothing time constant (0-1)\n * Higher values = smoother but slower response\n * Default: 0.8\n */\n smoothingTimeConstant?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current audio level (0-1)\n * 0 = silence, 1 = maximum level\n */\n level: number;\n\n /**\n * Peak level since last reset (0-1)\n */\n peakLevel: number;\n\n /**\n * Reset the peak level\n */\n resetPeak: () => void;\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 level and peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { level, peakLevel, resetPeak } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, smoothingTimeConstant = 0.8 } = options;\n\n const [level, setLevel] = useState(0);\n const [peakLevel, setPeakLevel] = useState(0);\n\n const meterRef = useRef<Meter | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const animationFrameRef = useRef<number | null>(null);\n\n const resetPeak = () => setPeakLevel(0);\n\n useEffect(() => {\n if (!stream) {\n setLevel(0);\n setPeakLevel(0);\n return;\n }\n\n let isMounted = true;\n\n // Setup audio monitoring\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n // Get Tone's context and resume if needed\n const context = getContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n if (!isMounted) return;\n\n // Create Tone.js Meter for level monitoring\n // Pass context to ensure it's created in the same context as the source\n const meter = new Meter({ smoothing: smoothingTimeConstant, context });\n meterRef.current = meter;\n\n // Create MediaStreamSource from the SAME context as the meter\n // Note: This creates a separate source from useRecording, but that's OK\n // since we're only using it for level monitoring (not recording)\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n\n // Connect source to meter using Tone's connect function\n connect(source, meter);\n\n // Start level monitoring\n const updateInterval = 1000 / updateRate;\n let lastUpdateTime = 0;\n\n const updateLevel = (timestamp: number) => {\n if (!isMounted || !meterRef.current) return;\n\n if (timestamp - lastUpdateTime >= updateInterval) {\n lastUpdateTime = timestamp;\n\n // Meter.getValue() returns dB, convert to 0-1 range\n const db = meterRef.current.getValue();\n const dbValue = typeof db === 'number' ? db : db[0];\n // dB is typically -Infinity to 0, map -100dB..0dB to 0..1\n // Using -100dB as floor since Firefox seems to report lower values\n const normalized = Math.max(0, Math.min(1, (dbValue + 100) / 100));\n\n setLevel(normalized);\n setPeakLevel((prev) => Math.max(prev, normalized));\n }\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n setupMonitoring();\n\n // Cleanup\n return () => {\n isMounted = false;\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Disconnect and clean up\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch {\n // Ignore disconnect errors\n }\n sourceRef.current = null;\n }\n\n if (meterRef.current) {\n meterRef.current.dispose();\n meterRef.current = null;\n }\n };\n }, [stream, smoothingTimeConstant, updateRate]);\n\n return {\n level,\n peakLevel,\n resetPeak,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect } 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 { resumeGlobalAudioContext } 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 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 // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const { level, peakLevel } = useMicrophoneLevel(stream);\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 const startRecording = useCallback(async () => {\n if (!selectedTrackId) {\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 await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [selectedTrackId, 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 if (buffer && selectedTrackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === selectedTrackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${selectedTrackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.error(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Calculate start position: max(currentTime, lastClipEndTime)\n const currentTimeSamples = Math.floor(currentTime * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n // Find the end time of the last clip (in samples)\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n // Use whichever is greater: cursor position or last clip end\n const startSample = Math.max(currentTimeSamples, lastClipEndSample);\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: buffer.length,\n offsetSamples: 0,\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 }, [selectedTrackId, tracks, setTracks, currentTime, stopRec]);\n\n // Auto-select the first device when devices become available\n useEffect(() => {\n // Only auto-select if we have permission, devices are available, and nothing is selected yet\n if (hasPermission && devices.length > 0 && selectedDevice === null) {\n setSelectedDevice(devices[0].deviceId);\n }\n }, [hasPermission, devices, selectedDevice]);\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 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]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n error: hookError || micError || 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","/**\n * RecordButton - Control button for starting/stopping recording\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordButtonProps {\n isRecording: boolean;\n onClick: () => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Button = styled.button<{ $isRecording: boolean }>`\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border: none;\n border-radius: 0.25rem;\n cursor: pointer;\n transition: all 0.2s ease-in-out;\n background: ${(props) => (props.$isRecording ? '#dc3545' : '#e74c3c')};\n color: white;\n\n &:hover:not(:disabled) {\n background: ${(props) => (props.$isRecording ? '#c82333' : '#c0392b')};\n transform: translateY(-1px);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n &:focus {\n outline: none;\n box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.3);\n }\n`;\n\nconst RecordingIndicator = styled.span`\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: white;\n margin-right: 0.5rem;\n animation: pulse 1.5s ease-in-out infinite;\n\n @keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n`;\n\nexport const RecordButton: React.FC<RecordButtonProps> = ({\n isRecording,\n onClick,\n disabled = false,\n className,\n}) => {\n return (\n <Button\n $isRecording={isRecording}\n onClick={onClick}\n disabled={disabled}\n className={className}\n aria-label={isRecording ? 'Stop recording' : 'Start recording'}\n >\n {isRecording && <RecordingIndicator />}\n {isRecording ? 'Stop Recording' : 'Record'}\n </Button>\n );\n};\n","/**\n * MicrophoneSelector - Dropdown for selecting microphone input device\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\nimport { BaseSelect, BaseLabel } from '@waveform-playlist/ui-components';\nimport { MicrophoneDevice } from '../types';\n\nexport interface MicrophoneSelectorProps {\n devices: MicrophoneDevice[];\n selectedDeviceId?: string;\n onDeviceChange: (deviceId: string) => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Select = styled(BaseSelect)`\n min-width: 200px;\n`;\n\nconst Label = styled(BaseLabel)`\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n`;\n\nexport const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = ({\n devices,\n selectedDeviceId,\n onDeviceChange,\n disabled = false,\n className,\n}) => {\n const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {\n onDeviceChange(event.target.value);\n };\n\n // Use first device if no selection provided\n const currentValue = selectedDeviceId || (devices.length > 0 ? devices[0].deviceId : '');\n\n return (\n <Label className={className}>\n Microphone\n <Select\n value={currentValue}\n onChange={handleChange}\n disabled={disabled || devices.length === 0}\n >\n {devices.length === 0 ? (\n <option value=\"\">No microphones found</option>\n ) : (\n devices.map((device) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))\n )}\n </Select>\n </Label>\n );\n};\n","/**\n * RecordingIndicator - Shows recording status, duration, and visual indicator\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordingIndicatorProps {\n isRecording: boolean;\n isPaused?: boolean;\n duration: number; // in seconds\n formatTime?: (seconds: number) => string;\n className?: string;\n}\n\nconst Container = styled.div<{ $isRecording: boolean }>`\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.5rem 0.75rem;\n background: ${(props) => (props.$isRecording ? '#fff3cd' : 'transparent')};\n border-radius: 0.25rem;\n transition: background 0.2s ease-in-out;\n`;\n\nconst Dot = styled.div<{ $isRecording: boolean; $isPaused: boolean }>`\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n opacity: ${(props) => (props.$isRecording ? 1 : 0)};\n transition: opacity 0.2s ease-in-out;\n\n ${(props) =>\n props.$isRecording &&\n !props.$isPaused &&\n `\n animation: blink 1.5s ease-in-out infinite;\n\n @keyframes blink {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n `}\n`;\n\nconst Duration = styled.span`\n font-family: 'Courier New', Monaco, monospace;\n font-size: 1rem;\n font-weight: 600;\n color: #495057;\n min-width: 70px;\n`;\n\nconst Status = styled.span<{ $isPaused: boolean }>`\n font-size: 0.75rem;\n font-weight: 500;\n color: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n text-transform: uppercase;\n`;\n\nconst defaultFormatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n};\n\nexport const RecordingIndicator: React.FC<RecordingIndicatorProps> = ({\n isRecording,\n isPaused = false,\n duration,\n formatTime = defaultFormatTime,\n className,\n}) => {\n return (\n <Container $isRecording={isRecording} className={className}>\n <Dot $isRecording={isRecording} $isPaused={isPaused} />\n <Duration>{formatTime(duration)}</Duration>\n {isRecording && <Status $isPaused={isPaused}>{isPaused ? 'Paused' : 'Recording'}</Status>}\n </Container>\n );\n};\n","/**\n * VU Meter Component\n *\n * Displays real-time audio input levels with color-coded zones\n * and peak indicator.\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface VUMeterProps {\n /**\n * Current audio level (0-1)\n */\n level: number;\n\n /**\n * Peak level (0-1)\n * Optional - if provided, shows peak indicator\n */\n peakLevel?: number;\n\n /**\n * Width of the meter in pixels\n * Default: 200\n */\n width?: number;\n\n /**\n * Height of the meter in pixels\n * Default: 20\n */\n height?: number;\n\n /**\n * Additional CSS class name\n */\n className?: string;\n}\n\nconst MeterContainer = styled.div<{ $width: number; $height: number }>`\n position: relative;\n width: ${(props) => props.$width}px;\n height: ${(props) => props.$height}px;\n background: #2c3e50;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);\n`;\n\n// Helper to get gradient color based on level\nconst getLevelGradient = (level: number): string => {\n if (level < 0.6) return 'linear-gradient(90deg, #27ae60, #2ecc71)';\n if (level < 0.85) return 'linear-gradient(90deg, #f39c12, #f1c40f)';\n return 'linear-gradient(90deg, #c0392b, #e74c3c)';\n};\n\n// Use .attrs() for frequently changing styles to avoid generating new CSS classes\nconst MeterFill = styled.div.attrs<{ $level: number; $height: number }>((props) => ({\n style: {\n width: `${props.$level * 100}%`,\n height: `${props.$height}px`,\n background: getLevelGradient(props.$level),\n boxShadow: props.$level > 0.01 ? '0 0 8px rgba(255, 255, 255, 0.3)' : 'none',\n },\n}))<{ $level: number; $height: number }>`\n position: absolute;\n left: 0;\n top: 0;\n transition:\n width 0.05s ease-out,\n background 0.1s ease-out;\n`;\n\n// Use .attrs() for frequently changing left position\nconst PeakIndicator = styled.div.attrs<{ $peakLevel: number; $height: number }>((props) => ({\n style: {\n left: `${props.$peakLevel * 100}%`,\n height: `${props.$height}px`,\n },\n}))<{ $peakLevel: number; $height: number }>`\n position: absolute;\n top: 0;\n width: 2px;\n background: #ecf0f1;\n box-shadow: 0 0 4px rgba(236, 240, 241, 0.8);\n transition: left 0.1s ease-out;\n`;\n\nconst ScaleMarkers = styled.div<{ $height: number }>`\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: ${(props) => props.$height}px;\n pointer-events: none;\n`;\n\nconst ScaleMark = styled.div<{ $position: number; $height: number }>`\n position: absolute;\n left: ${(props) => props.$position}%;\n top: 0;\n width: 1px;\n height: ${(props) => props.$height}px;\n background: rgba(255, 255, 255, 0.2);\n`;\n\n/**\n * VU Meter component for displaying audio input levels\n *\n * @example\n * ```typescript\n * import { useMicrophoneLevel, VUMeter } from '@waveform-playlist/recording';\n *\n * const { level, peakLevel } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} width={300} height={24} />;\n * ```\n */\nconst VUMeterComponent: React.FC<VUMeterProps> = ({\n level,\n peakLevel,\n width = 200,\n height = 20,\n className,\n}) => {\n // Clamp values to 0-1 range\n const clampedLevel = Math.max(0, Math.min(1, level));\n const clampedPeak = peakLevel !== undefined ? Math.max(0, Math.min(1, peakLevel)) : 0;\n\n return (\n <MeterContainer $width={width} $height={height} className={className}>\n <MeterFill $level={clampedLevel} $height={height} />\n\n {peakLevel !== undefined && clampedPeak > 0 && (\n <PeakIndicator $peakLevel={clampedPeak} $height={height} />\n )}\n\n <ScaleMarkers $height={height}>\n <ScaleMark $position={60} $height={height} />\n <ScaleMark $position={85} $height={height} />\n </ScaleMarkers>\n </MeterContainer>\n );\n};\n\n// Memoize to prevent unnecessary re-renders when parent updates\nexport const VUMeter = React.memo(VUMeterComponent);\n"],"mappings":"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","names":["newPeaks","result","useState","useEffect","useCallback","useEffect","useState","useRef","getContext","useState","useCallback","useEffect","useState","useCallback","useEffect","styled","jsx","jsxs","styled","jsx","jsxs","RecordingIndicator","styled","jsx","jsxs"]}
@@ -37,23 +37,35 @@ var RecordingProcessor = class extends AudioWorkletProcessor {
37
37
  return true;
38
38
  }
39
39
  const frameCount = input[0].length;
40
- for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {
41
- const inputChannel = input[channel];
42
- const buffer = this.buffers[channel];
43
- for (let i = 0; i < frameCount; i++) {
44
- buffer[this.samplesCollected + i] = inputChannel[i];
45
- }
40
+ if (this.bufferSize <= 0) {
41
+ return true;
46
42
  }
47
- this.samplesCollected += frameCount;
48
- if (this.samplesCollected >= this.bufferSize) {
49
- this.flushBuffers();
43
+ let offset = 0;
44
+ while (offset < frameCount) {
45
+ const remaining = this.bufferSize - this.samplesCollected;
46
+ const toCopy = Math.min(remaining, frameCount - offset);
47
+ for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {
48
+ const inputChannel = input[channel];
49
+ const buffer = this.buffers[channel];
50
+ for (let i = 0; i < toCopy; i++) {
51
+ buffer[this.samplesCollected + i] = inputChannel[offset + i];
52
+ }
53
+ }
54
+ this.samplesCollected += toCopy;
55
+ offset += toCopy;
56
+ if (this.samplesCollected >= this.bufferSize) {
57
+ this.flushBuffers();
58
+ }
50
59
  }
51
60
  return true;
52
61
  }
53
62
  flushBuffers() {
54
- const samples = this.buffers[0].slice(0, this.samplesCollected);
63
+ const channels = [];
64
+ for (let i = 0; i < this.channelCount; i++) {
65
+ channels.push(this.buffers[i].slice(0, this.samplesCollected));
66
+ }
55
67
  this.port.postMessage({
56
- samples,
68
+ channels,
57
69
  sampleRate,
58
70
  channelCount: this.channelCount
59
71
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/worklet/recording-processor.worklet.ts"],"sourcesContent":["/**\n * RecordingProcessor - AudioWorklet processor for capturing raw audio data\n *\n * This processor runs in the AudioWorklet thread and captures audio samples\n * at the browser's native sample rate. It buffers samples and sends them to\n * the main thread at regular intervals (~16ms) for peak generation and\n * waveform visualization.\n *\n * Message Format (to main thread):\n * {\n * samples: Float32Array, // Audio samples for this chunk\n * sampleRate: number, // Sample rate of the audio\n * channelCount: number // Number of channels\n * }\n *\n * Note: VU meter levels are handled by AnalyserNode in useMicrophoneLevel hook,\n * not by this worklet.\n */\n\n// Type declarations for AudioWorklet context\ndeclare const sampleRate: number;\n\ninterface AudioParamDescriptor {\n name: string;\n defaultValue?: number;\n minValue?: number;\n maxValue?: number;\n automationRate?: 'a-rate' | 'k-rate';\n}\n\ndeclare class AudioWorkletProcessor {\n readonly port: MessagePort;\n process(\n inputs: Float32Array[][],\n outputs: Float32Array[][],\n parameters: Record<string, Float32Array>\n ): boolean;\n}\ndeclare function registerProcessor(\n name: string,\n processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & {\n parameterDescriptors?: AudioParamDescriptor[];\n }\n): void;\n\ninterface RecordingProcessorMessage {\n samples: Float32Array;\n sampleRate: number;\n channelCount: number;\n}\n\nclass RecordingProcessor extends AudioWorkletProcessor {\n private buffers: Float32Array[];\n private bufferSize: number;\n private samplesCollected: number;\n private isRecording: boolean;\n private channelCount: number;\n\n constructor() {\n super();\n\n // Buffer size for ~16ms at 48kHz (approximately one animation frame)\n // This will be adjusted based on actual sample rate\n this.bufferSize = 0;\n this.buffers = [];\n this.samplesCollected = 0;\n this.isRecording = false;\n this.channelCount = 1;\n\n // Listen for control messages from main thread\n this.port.onmessage = (event) => {\n const { command, sampleRate, channelCount } = event.data;\n\n if (command === 'start') {\n this.isRecording = true;\n this.channelCount = channelCount || 1;\n\n // Calculate buffer size for ~16ms chunks (60 fps)\n // At 48kHz: 48000 * 0.016 = 768 samples\n this.bufferSize = Math.floor((sampleRate || 48000) * 0.016);\n\n // Initialize buffers for each channel\n this.buffers = [];\n for (let i = 0; i < this.channelCount; i++) {\n this.buffers[i] = new Float32Array(this.bufferSize);\n }\n this.samplesCollected = 0;\n } else if (command === 'stop') {\n this.isRecording = false;\n\n // Send any remaining buffered samples\n if (this.samplesCollected > 0) {\n this.flushBuffers();\n }\n }\n };\n }\n\n process(\n inputs: Float32Array[][],\n _outputs: Float32Array[][],\n _parameters: Record<string, Float32Array>\n ): boolean {\n if (!this.isRecording) {\n return true; // Keep processor alive\n }\n\n const input = inputs[0];\n if (!input || input.length === 0) {\n return true; // No input yet, keep alive\n }\n\n const frameCount = input[0].length;\n\n // Process each channel\n for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {\n const inputChannel = input[channel];\n const buffer = this.buffers[channel];\n\n // Copy samples to buffer\n for (let i = 0; i < frameCount; i++) {\n buffer[this.samplesCollected + i] = inputChannel[i];\n }\n }\n\n this.samplesCollected += frameCount;\n\n // When buffer is full, send to main thread\n if (this.samplesCollected >= this.bufferSize) {\n this.flushBuffers();\n }\n\n return true; // Keep processor alive\n }\n\n private flushBuffers(): void {\n // For now, we'll mix down to mono or send the first channel\n // This simplifies peak generation and waveform display\n const samples = this.buffers[0].slice(0, this.samplesCollected);\n\n // Send to main thread\n this.port.postMessage({\n samples: samples,\n sampleRate: sampleRate,\n channelCount: this.channelCount,\n } as RecordingProcessorMessage);\n\n // Reset buffer\n this.samplesCollected = 0;\n }\n}\n\n// Register the processor\nregisterProcessor('recording-processor', RecordingProcessor);\n"],"mappings":";;;AAmDA,IAAM,qBAAN,cAAiC,sBAAsB;AAAA,EAOrD,cAAc;AACZ,UAAM;AAIN,SAAK,aAAa;AAClB,SAAK,UAAU,CAAC;AAChB,SAAK,mBAAmB;AACxB,SAAK,cAAc;AACnB,SAAK,eAAe;AAGpB,SAAK,KAAK,YAAY,CAAC,UAAU;AAC/B,YAAM,EAAE,SAAS,YAAAA,aAAY,aAAa,IAAI,MAAM;AAEpD,UAAI,YAAY,SAAS;AACvB,aAAK,cAAc;AACnB,aAAK,eAAe,gBAAgB;AAIpC,aAAK,aAAa,KAAK,OAAOA,eAAc,QAAS,KAAK;AAG1D,aAAK,UAAU,CAAC;AAChB,iBAAS,IAAI,GAAG,IAAI,KAAK,cAAc,KAAK;AAC1C,eAAK,QAAQ,CAAC,IAAI,IAAI,aAAa,KAAK,UAAU;AAAA,QACpD;AACA,aAAK,mBAAmB;AAAA,MAC1B,WAAW,YAAY,QAAQ;AAC7B,aAAK,cAAc;AAGnB,YAAI,KAAK,mBAAmB,GAAG;AAC7B,eAAK,aAAa;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QACE,QACA,UACA,aACS;AACT,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,OAAO,CAAC;AACtB,QAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,MAAM,CAAC,EAAE;AAG5B,aAAS,UAAU,GAAG,UAAU,KAAK,IAAI,MAAM,QAAQ,KAAK,YAAY,GAAG,WAAW;AACpF,YAAM,eAAe,MAAM,OAAO;AAClC,YAAM,SAAS,KAAK,QAAQ,OAAO;AAGnC,eAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,eAAO,KAAK,mBAAmB,CAAC,IAAI,aAAa,CAAC;AAAA,MACpD;AAAA,IACF;AAEA,SAAK,oBAAoB;AAGzB,QAAI,KAAK,oBAAoB,KAAK,YAAY;AAC5C,WAAK,aAAa;AAAA,IACpB;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAqB;AAG3B,UAAM,UAAU,KAAK,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,gBAAgB;AAG9D,SAAK,KAAK,YAAY;AAAA,MACpB;AAAA,MACA;AAAA,MACA,cAAc,KAAK;AAAA,IACrB,CAA8B;AAG9B,SAAK,mBAAmB;AAAA,EAC1B;AACF;AAGA,kBAAkB,uBAAuB,kBAAkB;","names":["sampleRate"]}
1
+ {"version":3,"sources":["../../src/worklet/recording-processor.worklet.ts"],"sourcesContent":["/**\n * RecordingProcessor - AudioWorklet processor for capturing raw audio data\n *\n * This processor runs in the AudioWorklet thread and captures audio samples\n * at the browser's native sample rate. It buffers samples and sends them to\n * the main thread at regular intervals (~16ms) for peak generation and\n * waveform visualization.\n *\n * Message Format (to main thread):\n * {\n * channels: Float32Array[], // Per-channel audio samples for this chunk\n * sampleRate: number, // Sample rate of the audio\n * channelCount: number // Number of channels\n * }\n *\n * Note: VU meter levels are handled by AnalyserNode in useMicrophoneLevel hook,\n * not by this worklet.\n */\n\n// Type declarations for AudioWorklet context\ndeclare const sampleRate: number;\n\ninterface AudioParamDescriptor {\n name: string;\n defaultValue?: number;\n minValue?: number;\n maxValue?: number;\n automationRate?: 'a-rate' | 'k-rate';\n}\n\ndeclare class AudioWorkletProcessor {\n readonly port: MessagePort;\n process(\n inputs: Float32Array[][],\n outputs: Float32Array[][],\n parameters: Record<string, Float32Array>\n ): boolean;\n}\ndeclare function registerProcessor(\n name: string,\n processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & {\n parameterDescriptors?: AudioParamDescriptor[];\n }\n): void;\n\ninterface RecordingProcessorMessage {\n channels: Float32Array[];\n sampleRate: number;\n channelCount: number;\n}\n\nclass RecordingProcessor extends AudioWorkletProcessor {\n private buffers: Float32Array[];\n private bufferSize: number;\n private samplesCollected: number;\n private isRecording: boolean;\n private channelCount: number;\n\n constructor() {\n super();\n\n // Buffer size for ~16ms at 48kHz (approximately one animation frame)\n // This will be adjusted based on actual sample rate\n this.bufferSize = 0;\n this.buffers = [];\n this.samplesCollected = 0;\n this.isRecording = false;\n this.channelCount = 1;\n\n // Listen for control messages from main thread\n this.port.onmessage = (event) => {\n const { command, sampleRate, channelCount } = event.data;\n\n if (command === 'start') {\n this.isRecording = true;\n this.channelCount = channelCount || 1;\n\n // Calculate buffer size for ~16ms chunks (60 fps)\n // At 48kHz: 48000 * 0.016 = 768 samples\n this.bufferSize = Math.floor((sampleRate || 48000) * 0.016);\n\n // Initialize buffers for each channel\n this.buffers = [];\n for (let i = 0; i < this.channelCount; i++) {\n this.buffers[i] = new Float32Array(this.bufferSize);\n }\n this.samplesCollected = 0;\n } else if (command === 'stop') {\n this.isRecording = false;\n\n // Send any remaining buffered samples\n if (this.samplesCollected > 0) {\n this.flushBuffers();\n }\n }\n };\n }\n\n process(\n inputs: Float32Array[][],\n _outputs: Float32Array[][],\n _parameters: Record<string, Float32Array>\n ): boolean {\n if (!this.isRecording) {\n return true; // Keep processor alive\n }\n\n const input = inputs[0];\n if (!input || input.length === 0) {\n return true; // No input yet, keep alive\n }\n\n const frameCount = input[0].length;\n\n if (this.bufferSize <= 0) {\n return true; // Not yet configured via 'start' command\n }\n\n let offset = 0;\n\n // Process samples in chunks that fit within the buffer.\n // The AudioWorklet quantum (128 samples) may not divide evenly into\n // bufferSize (e.g., 705 at 44100Hz), so a single frame can cross\n // the buffer boundary. Without this loop, samples beyond bufferSize\n // are silently dropped by the typed array, causing audio gaps.\n while (offset < frameCount) {\n const remaining = this.bufferSize - this.samplesCollected;\n const toCopy = Math.min(remaining, frameCount - offset);\n\n for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {\n const inputChannel = input[channel];\n const buffer = this.buffers[channel];\n\n for (let i = 0; i < toCopy; i++) {\n buffer[this.samplesCollected + i] = inputChannel[offset + i];\n }\n }\n\n this.samplesCollected += toCopy;\n offset += toCopy;\n\n // When buffer is full, send to main thread\n if (this.samplesCollected >= this.bufferSize) {\n this.flushBuffers();\n }\n }\n\n return true; // Keep processor alive\n }\n\n private flushBuffers(): void {\n // Send all channel buffers to main thread\n const channels: Float32Array[] = [];\n for (let i = 0; i < this.channelCount; i++) {\n channels.push(this.buffers[i].slice(0, this.samplesCollected));\n }\n\n this.port.postMessage({\n channels,\n sampleRate: sampleRate,\n channelCount: this.channelCount,\n } as RecordingProcessorMessage);\n\n // Reset buffer\n this.samplesCollected = 0;\n }\n}\n\n// Register the processor\nregisterProcessor('recording-processor', RecordingProcessor);\n"],"mappings":";;;AAmDA,IAAM,qBAAN,cAAiC,sBAAsB;AAAA,EAOrD,cAAc;AACZ,UAAM;AAIN,SAAK,aAAa;AAClB,SAAK,UAAU,CAAC;AAChB,SAAK,mBAAmB;AACxB,SAAK,cAAc;AACnB,SAAK,eAAe;AAGpB,SAAK,KAAK,YAAY,CAAC,UAAU;AAC/B,YAAM,EAAE,SAAS,YAAAA,aAAY,aAAa,IAAI,MAAM;AAEpD,UAAI,YAAY,SAAS;AACvB,aAAK,cAAc;AACnB,aAAK,eAAe,gBAAgB;AAIpC,aAAK,aAAa,KAAK,OAAOA,eAAc,QAAS,KAAK;AAG1D,aAAK,UAAU,CAAC;AAChB,iBAAS,IAAI,GAAG,IAAI,KAAK,cAAc,KAAK;AAC1C,eAAK,QAAQ,CAAC,IAAI,IAAI,aAAa,KAAK,UAAU;AAAA,QACpD;AACA,aAAK,mBAAmB;AAAA,MAC1B,WAAW,YAAY,QAAQ;AAC7B,aAAK,cAAc;AAGnB,YAAI,KAAK,mBAAmB,GAAG;AAC7B,eAAK,aAAa;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QACE,QACA,UACA,aACS;AACT,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,OAAO,CAAC;AACtB,QAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,MAAM,CAAC,EAAE;AAE5B,QAAI,KAAK,cAAc,GAAG;AACxB,aAAO;AAAA,IACT;AAEA,QAAI,SAAS;AAOb,WAAO,SAAS,YAAY;AAC1B,YAAM,YAAY,KAAK,aAAa,KAAK;AACzC,YAAM,SAAS,KAAK,IAAI,WAAW,aAAa,MAAM;AAEtD,eAAS,UAAU,GAAG,UAAU,KAAK,IAAI,MAAM,QAAQ,KAAK,YAAY,GAAG,WAAW;AACpF,cAAM,eAAe,MAAM,OAAO;AAClC,cAAM,SAAS,KAAK,QAAQ,OAAO;AAEnC,iBAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,iBAAO,KAAK,mBAAmB,CAAC,IAAI,aAAa,SAAS,CAAC;AAAA,QAC7D;AAAA,MACF;AAEA,WAAK,oBAAoB;AACzB,gBAAU;AAGV,UAAI,KAAK,oBAAoB,KAAK,YAAY;AAC5C,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAqB;AAE3B,UAAM,WAA2B,CAAC;AAClC,aAAS,IAAI,GAAG,IAAI,KAAK,cAAc,KAAK;AAC1C,eAAS,KAAK,KAAK,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,gBAAgB,CAAC;AAAA,IAC/D;AAEA,SAAK,KAAK,YAAY;AAAA,MACpB;AAAA,MACA;AAAA,MACA,cAAc,KAAK;AAAA,IACrB,CAA8B;AAG9B,SAAK,mBAAmB;AAAA,EAC1B;AACF;AAGA,kBAAkB,uBAAuB,kBAAkB;","names":["sampleRate"]}
@@ -35,23 +35,35 @@ var RecordingProcessor = class extends AudioWorkletProcessor {
35
35
  return true;
36
36
  }
37
37
  const frameCount = input[0].length;
38
- for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {
39
- const inputChannel = input[channel];
40
- const buffer = this.buffers[channel];
41
- for (let i = 0; i < frameCount; i++) {
42
- buffer[this.samplesCollected + i] = inputChannel[i];
43
- }
38
+ if (this.bufferSize <= 0) {
39
+ return true;
44
40
  }
45
- this.samplesCollected += frameCount;
46
- if (this.samplesCollected >= this.bufferSize) {
47
- this.flushBuffers();
41
+ let offset = 0;
42
+ while (offset < frameCount) {
43
+ const remaining = this.bufferSize - this.samplesCollected;
44
+ const toCopy = Math.min(remaining, frameCount - offset);
45
+ for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {
46
+ const inputChannel = input[channel];
47
+ const buffer = this.buffers[channel];
48
+ for (let i = 0; i < toCopy; i++) {
49
+ buffer[this.samplesCollected + i] = inputChannel[offset + i];
50
+ }
51
+ }
52
+ this.samplesCollected += toCopy;
53
+ offset += toCopy;
54
+ if (this.samplesCollected >= this.bufferSize) {
55
+ this.flushBuffers();
56
+ }
48
57
  }
49
58
  return true;
50
59
  }
51
60
  flushBuffers() {
52
- const samples = this.buffers[0].slice(0, this.samplesCollected);
61
+ const channels = [];
62
+ for (let i = 0; i < this.channelCount; i++) {
63
+ channels.push(this.buffers[i].slice(0, this.samplesCollected));
64
+ }
53
65
  this.port.postMessage({
54
- samples,
66
+ channels,
55
67
  sampleRate,
56
68
  channelCount: this.channelCount
57
69
  });