@waveform-playlist/recording 7.1.2 → 7.1.3
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.js
CHANGED
|
@@ -59,11 +59,7 @@ function concatenateAudioData(chunks) {
|
|
|
59
59
|
return result;
|
|
60
60
|
}
|
|
61
61
|
function createAudioBuffer(audioContext, samples, sampleRate, channelCount = 1) {
|
|
62
|
-
const buffer = audioContext.createBuffer(
|
|
63
|
-
channelCount,
|
|
64
|
-
samples.length,
|
|
65
|
-
sampleRate
|
|
66
|
-
);
|
|
62
|
+
const buffer = audioContext.createBuffer(channelCount, samples.length, sampleRate);
|
|
67
63
|
const typedSamples = new Float32Array(samples);
|
|
68
64
|
buffer.copyToChannel(typedSamples, 0);
|
|
69
65
|
return buffer;
|
|
@@ -125,10 +121,7 @@ function appendPeaks(existingPeaks, newSamples, samplesPerPixel, totalSamplesPro
|
|
|
125
121
|
var import_tone = require("tone");
|
|
126
122
|
var import_meta = {};
|
|
127
123
|
function useRecording(stream, options = {}) {
|
|
128
|
-
const {
|
|
129
|
-
channelCount = 1,
|
|
130
|
-
samplesPerPixel = 1024
|
|
131
|
-
} = options;
|
|
124
|
+
const { channelCount = 1, samplesPerPixel = 1024 } = options;
|
|
132
125
|
const [isRecording, setIsRecording] = (0, import_react.useState)(false);
|
|
133
126
|
const [isPaused, setIsPaused] = (0, import_react.useState)(false);
|
|
134
127
|
const [duration, setDuration] = (0, import_react.useState)(0);
|
|
@@ -153,10 +146,7 @@ function useRecording(stream, options = {}) {
|
|
|
153
146
|
}
|
|
154
147
|
try {
|
|
155
148
|
const context = (0, import_tone.getContext)();
|
|
156
|
-
const workletUrl = new URL(
|
|
157
|
-
"./worklet/recording-processor.worklet.js",
|
|
158
|
-
import_meta.url
|
|
159
|
-
).href;
|
|
149
|
+
const workletUrl = new URL("./worklet/recording-processor.worklet.js", import_meta.url).href;
|
|
160
150
|
await context.addAudioWorkletModule(workletUrl);
|
|
161
151
|
workletLoadedRef.current = true;
|
|
162
152
|
} catch (err) {
|
|
@@ -246,12 +236,7 @@ function useRecording(stream, options = {}) {
|
|
|
246
236
|
const allSamples = concatenateAudioData(recordedChunksRef.current);
|
|
247
237
|
const context = (0, import_tone.getContext)();
|
|
248
238
|
const rawContext = context.rawContext;
|
|
249
|
-
const buffer = createAudioBuffer(
|
|
250
|
-
rawContext,
|
|
251
|
-
allSamples,
|
|
252
|
-
rawContext.sampleRate,
|
|
253
|
-
channelCount
|
|
254
|
-
);
|
|
239
|
+
const buffer = createAudioBuffer(rawContext, allSamples, rawContext.sampleRate, channelCount);
|
|
255
240
|
setAudioBuffer(buffer);
|
|
256
241
|
setDuration(buffer.duration);
|
|
257
242
|
isRecordingRef.current = false;
|
|
@@ -346,43 +331,44 @@ function useMicrophoneAccess() {
|
|
|
346
331
|
setError(err instanceof Error ? err : new Error("Failed to enumerate devices"));
|
|
347
332
|
}
|
|
348
333
|
}, []);
|
|
349
|
-
const requestAccess = (0, import_react2.useCallback)(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
stream
|
|
334
|
+
const requestAccess = (0, import_react2.useCallback)(
|
|
335
|
+
async (deviceId, audioConstraints) => {
|
|
336
|
+
setIsLoading(true);
|
|
337
|
+
setError(null);
|
|
338
|
+
try {
|
|
339
|
+
if (stream) {
|
|
340
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
341
|
+
}
|
|
342
|
+
const audio = {
|
|
343
|
+
// Recording-optimized defaults: prioritize raw audio quality and low latency
|
|
344
|
+
echoCancellation: false,
|
|
345
|
+
noiseSuppression: false,
|
|
346
|
+
autoGainControl: false,
|
|
347
|
+
latency: 0,
|
|
348
|
+
// Low latency mode (not in TS types yet, but supported in modern browsers)
|
|
349
|
+
// User-provided constraints override defaults
|
|
350
|
+
...audioConstraints,
|
|
351
|
+
// Device ID override (if specified)
|
|
352
|
+
...deviceId && { deviceId: { exact: deviceId } }
|
|
353
|
+
};
|
|
354
|
+
const constraints = {
|
|
355
|
+
audio,
|
|
356
|
+
video: false
|
|
357
|
+
};
|
|
358
|
+
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
359
|
+
setStream(newStream);
|
|
360
|
+
setHasPermission(true);
|
|
361
|
+
await enumerateDevices();
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.error("Failed to access microphone:", err);
|
|
364
|
+
setError(err instanceof Error ? err : new Error("Failed to access microphone"));
|
|
365
|
+
setHasPermission(false);
|
|
366
|
+
} finally {
|
|
367
|
+
setIsLoading(false);
|
|
355
368
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
noiseSuppression: false,
|
|
360
|
-
autoGainControl: false,
|
|
361
|
-
latency: 0,
|
|
362
|
-
// Low latency mode (not in TS types yet, but supported in modern browsers)
|
|
363
|
-
// User-provided constraints override defaults
|
|
364
|
-
...audioConstraints,
|
|
365
|
-
// Device ID override (if specified)
|
|
366
|
-
...deviceId && { deviceId: { exact: deviceId } }
|
|
367
|
-
};
|
|
368
|
-
const constraints = {
|
|
369
|
-
audio,
|
|
370
|
-
video: false
|
|
371
|
-
};
|
|
372
|
-
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
373
|
-
setStream(newStream);
|
|
374
|
-
setHasPermission(true);
|
|
375
|
-
await enumerateDevices();
|
|
376
|
-
} catch (err) {
|
|
377
|
-
console.error("Failed to access microphone:", err);
|
|
378
|
-
setError(
|
|
379
|
-
err instanceof Error ? err : new Error("Failed to access microphone")
|
|
380
|
-
);
|
|
381
|
-
setHasPermission(false);
|
|
382
|
-
} finally {
|
|
383
|
-
setIsLoading(false);
|
|
384
|
-
}
|
|
385
|
-
}, [stream, enumerateDevices]);
|
|
369
|
+
},
|
|
370
|
+
[stream, enumerateDevices]
|
|
371
|
+
);
|
|
386
372
|
const stopStream = (0, import_react2.useCallback)(() => {
|
|
387
373
|
if (stream) {
|
|
388
374
|
stream.getTracks().forEach((track) => track.stop());
|
|
@@ -413,10 +399,7 @@ function useMicrophoneAccess() {
|
|
|
413
399
|
var import_react3 = require("react");
|
|
414
400
|
var import_tone2 = require("tone");
|
|
415
401
|
function useMicrophoneLevel(stream, options = {}) {
|
|
416
|
-
const {
|
|
417
|
-
updateRate = 60,
|
|
418
|
-
smoothingTimeConstant = 0.8
|
|
419
|
-
} = options;
|
|
402
|
+
const { updateRate = 60, smoothingTimeConstant = 0.8 } = options;
|
|
420
403
|
const [level, setLevel] = (0, import_react3.useState)(0);
|
|
421
404
|
const [peakLevel, setPeakLevel] = (0, import_react3.useState)(0);
|
|
422
405
|
const meterRef = (0, import_react3.useRef)(null);
|
|
@@ -493,13 +476,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
493
476
|
const [isMonitoring, setIsMonitoring] = (0, import_react4.useState)(false);
|
|
494
477
|
const [selectedDevice, setSelectedDevice] = (0, import_react4.useState)(null);
|
|
495
478
|
const [hookError, setHookError] = (0, import_react4.useState)(null);
|
|
496
|
-
const {
|
|
497
|
-
stream,
|
|
498
|
-
devices,
|
|
499
|
-
hasPermission,
|
|
500
|
-
requestAccess,
|
|
501
|
-
error: micError
|
|
502
|
-
} = useMicrophoneAccess();
|
|
479
|
+
const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();
|
|
503
480
|
const { level, peakLevel } = useMicrophoneLevel(stream);
|
|
504
481
|
const {
|
|
505
482
|
isRecording,
|
|
@@ -515,7 +492,9 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
515
492
|
} = useRecording(stream, recordingOptions);
|
|
516
493
|
const startRecording = (0, import_react4.useCallback)(async () => {
|
|
517
494
|
if (!selectedTrackId) {
|
|
518
|
-
setHookError(
|
|
495
|
+
setHookError(
|
|
496
|
+
new Error("Cannot start recording: no track selected. Select or create a track first.")
|
|
497
|
+
);
|
|
519
498
|
return;
|
|
520
499
|
}
|
|
521
500
|
try {
|
|
@@ -595,17 +574,20 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
595
574
|
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
596
575
|
}
|
|
597
576
|
}, [requestAccess, audioConstraints]);
|
|
598
|
-
const changeDevice = (0, import_react4.useCallback)(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
577
|
+
const changeDevice = (0, import_react4.useCallback)(
|
|
578
|
+
async (deviceId) => {
|
|
579
|
+
try {
|
|
580
|
+
setHookError(null);
|
|
581
|
+
setSelectedDevice(deviceId);
|
|
582
|
+
await requestAccess(deviceId, audioConstraints);
|
|
583
|
+
await (0, import_playout.resumeGlobalAudioContext)();
|
|
584
|
+
setIsMonitoring(true);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
[requestAccess, audioConstraints]
|
|
590
|
+
);
|
|
609
591
|
return {
|
|
610
592
|
// Recording state
|
|
611
593
|
isRecording,
|
|
@@ -837,7 +819,9 @@ var MeterFill = import_styled_components4.default.div.attrs((props) => ({
|
|
|
837
819
|
position: absolute;
|
|
838
820
|
left: 0;
|
|
839
821
|
top: 0;
|
|
840
|
-
transition:
|
|
822
|
+
transition:
|
|
823
|
+
width 0.05s ease-out,
|
|
824
|
+
background 0.1s ease-out;
|
|
841
825
|
`;
|
|
842
826
|
var PeakIndicator = import_styled_components4.default.div.attrs((props) => ({
|
|
843
827
|
style: {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts","../src/components/RecordButton.tsx","../src/components/MicrophoneSelector.tsx","../src/components/RecordingIndicator.tsx","../src/components/VUMeter.tsx"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport { useRecording, useMicrophoneAccess, useMicrophoneLevel, useIntegratedRecording } from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n UseIntegratedRecordingReturn,\n IntegratedRecordingOptions,\n} from './hooks';\n\n// Components\nexport { RecordButton, MicrophoneSelector, RecordingIndicator, VUMeter } from './components';\nexport type {\n RecordButtonProps,\n MicrophoneSelectorProps,\n RecordingIndicatorProps,\n VUMeterProps,\n} from './components';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n\n// Utilities\nexport { generatePeaks } from './utils/peaksGenerator';\nexport { createAudioBuffer, concatenateAudioData } from './utils/audioBufferUtils';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getContext } from 'tone';\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const {\n channelCount = 1,\n samplesPerPixel = 1024,\n } = 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(\n './worklet/recording-processor.worklet.js',\n import.meta.url\n ).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(\n rawContext,\n allSamples,\n rawContext.sampleRate,\n channelCount\n );\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(\n channelCount,\n samples.length,\n sampleRate\n );\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 peakArray[i * 2] = Math.floor(min * maxValue);\n peakArray[i * 2 + 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.floor(min * maxValue);\n updated[existingPeaks.length - 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(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(\n err instanceof Error ? err : new Error('Failed to access microphone')\n );\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n }, [stream, enumerateDevices]);\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 {\n updateRate = 60,\n smoothingTimeConstant = 0.8,\n } = 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 {\n stream,\n devices,\n hasPermission,\n requestAccess,\n error: micError,\n } = 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(new Error('Cannot start recording: no track selected. Select or create a track first.'));\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(clip =>\n 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(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 }, [requestAccess, audioConstraints]);\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: width 0.05s ease-out, 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\n ? Math.max(0, Math.min(1, peakLevel))\n : 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":["RecordingIndicator","newPeaks","result","import_react","import_react","import_tone","import_react","styled","import_styled_components","import_jsx_runtime","styled","import_styled_components","import_jsx_runtime","styled","RecordingIndicator","import_react","import_styled_components","import_jsx_runtime","styled","React"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts","../src/components/RecordButton.tsx","../src/components/MicrophoneSelector.tsx","../src/components/RecordingIndicator.tsx","../src/components/VUMeter.tsx"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport {\n useRecording,\n useMicrophoneAccess,\n useMicrophoneLevel,\n useIntegratedRecording,\n} from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n UseIntegratedRecordingReturn,\n IntegratedRecordingOptions,\n} from './hooks';\n\n// Components\nexport { RecordButton, MicrophoneSelector, RecordingIndicator, VUMeter } from './components';\nexport type {\n RecordButtonProps,\n MicrophoneSelectorProps,\n RecordingIndicatorProps,\n VUMeterProps,\n} from './components';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n\n// Utilities\nexport { generatePeaks } from './utils/peaksGenerator';\nexport { createAudioBuffer, concatenateAudioData } from './utils/audioBufferUtils';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { 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 peakArray[i * 2] = Math.floor(min * maxValue);\n peakArray[i * 2 + 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.floor(min * maxValue);\n updated[existingPeaks.length - 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":["RecordingIndicator","newPeaks","result","import_react","import_react","import_tone","import_react","styled","import_styled_components","import_jsx_runtime","styled","import_styled_components","import_jsx_runtime","styled","RecordingIndicator","import_react","import_styled_components","import_jsx_runtime","styled","React"]}
|
package/dist/index.mjs
CHANGED
|
@@ -13,11 +13,7 @@ function concatenateAudioData(chunks) {
|
|
|
13
13
|
return result;
|
|
14
14
|
}
|
|
15
15
|
function createAudioBuffer(audioContext, samples, sampleRate, channelCount = 1) {
|
|
16
|
-
const buffer = audioContext.createBuffer(
|
|
17
|
-
channelCount,
|
|
18
|
-
samples.length,
|
|
19
|
-
sampleRate
|
|
20
|
-
);
|
|
16
|
+
const buffer = audioContext.createBuffer(channelCount, samples.length, sampleRate);
|
|
21
17
|
const typedSamples = new Float32Array(samples);
|
|
22
18
|
buffer.copyToChannel(typedSamples, 0);
|
|
23
19
|
return buffer;
|
|
@@ -78,10 +74,7 @@ function appendPeaks(existingPeaks, newSamples, samplesPerPixel, totalSamplesPro
|
|
|
78
74
|
// src/hooks/useRecording.ts
|
|
79
75
|
import { getContext } from "tone";
|
|
80
76
|
function useRecording(stream, options = {}) {
|
|
81
|
-
const {
|
|
82
|
-
channelCount = 1,
|
|
83
|
-
samplesPerPixel = 1024
|
|
84
|
-
} = options;
|
|
77
|
+
const { channelCount = 1, samplesPerPixel = 1024 } = options;
|
|
85
78
|
const [isRecording, setIsRecording] = useState(false);
|
|
86
79
|
const [isPaused, setIsPaused] = useState(false);
|
|
87
80
|
const [duration, setDuration] = useState(0);
|
|
@@ -106,10 +99,7 @@ function useRecording(stream, options = {}) {
|
|
|
106
99
|
}
|
|
107
100
|
try {
|
|
108
101
|
const context = getContext();
|
|
109
|
-
const workletUrl = new URL(
|
|
110
|
-
"./worklet/recording-processor.worklet.js",
|
|
111
|
-
import.meta.url
|
|
112
|
-
).href;
|
|
102
|
+
const workletUrl = new URL("./worklet/recording-processor.worklet.js", import.meta.url).href;
|
|
113
103
|
await context.addAudioWorkletModule(workletUrl);
|
|
114
104
|
workletLoadedRef.current = true;
|
|
115
105
|
} catch (err) {
|
|
@@ -199,12 +189,7 @@ function useRecording(stream, options = {}) {
|
|
|
199
189
|
const allSamples = concatenateAudioData(recordedChunksRef.current);
|
|
200
190
|
const context = getContext();
|
|
201
191
|
const rawContext = context.rawContext;
|
|
202
|
-
const buffer = createAudioBuffer(
|
|
203
|
-
rawContext,
|
|
204
|
-
allSamples,
|
|
205
|
-
rawContext.sampleRate,
|
|
206
|
-
channelCount
|
|
207
|
-
);
|
|
192
|
+
const buffer = createAudioBuffer(rawContext, allSamples, rawContext.sampleRate, channelCount);
|
|
208
193
|
setAudioBuffer(buffer);
|
|
209
194
|
setDuration(buffer.duration);
|
|
210
195
|
isRecordingRef.current = false;
|
|
@@ -299,43 +284,44 @@ function useMicrophoneAccess() {
|
|
|
299
284
|
setError(err instanceof Error ? err : new Error("Failed to enumerate devices"));
|
|
300
285
|
}
|
|
301
286
|
}, []);
|
|
302
|
-
const requestAccess = useCallback2(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
stream
|
|
287
|
+
const requestAccess = useCallback2(
|
|
288
|
+
async (deviceId, audioConstraints) => {
|
|
289
|
+
setIsLoading(true);
|
|
290
|
+
setError(null);
|
|
291
|
+
try {
|
|
292
|
+
if (stream) {
|
|
293
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
294
|
+
}
|
|
295
|
+
const audio = {
|
|
296
|
+
// Recording-optimized defaults: prioritize raw audio quality and low latency
|
|
297
|
+
echoCancellation: false,
|
|
298
|
+
noiseSuppression: false,
|
|
299
|
+
autoGainControl: false,
|
|
300
|
+
latency: 0,
|
|
301
|
+
// Low latency mode (not in TS types yet, but supported in modern browsers)
|
|
302
|
+
// User-provided constraints override defaults
|
|
303
|
+
...audioConstraints,
|
|
304
|
+
// Device ID override (if specified)
|
|
305
|
+
...deviceId && { deviceId: { exact: deviceId } }
|
|
306
|
+
};
|
|
307
|
+
const constraints = {
|
|
308
|
+
audio,
|
|
309
|
+
video: false
|
|
310
|
+
};
|
|
311
|
+
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
312
|
+
setStream(newStream);
|
|
313
|
+
setHasPermission(true);
|
|
314
|
+
await enumerateDevices();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error("Failed to access microphone:", err);
|
|
317
|
+
setError(err instanceof Error ? err : new Error("Failed to access microphone"));
|
|
318
|
+
setHasPermission(false);
|
|
319
|
+
} finally {
|
|
320
|
+
setIsLoading(false);
|
|
308
321
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
noiseSuppression: false,
|
|
313
|
-
autoGainControl: false,
|
|
314
|
-
latency: 0,
|
|
315
|
-
// Low latency mode (not in TS types yet, but supported in modern browsers)
|
|
316
|
-
// User-provided constraints override defaults
|
|
317
|
-
...audioConstraints,
|
|
318
|
-
// Device ID override (if specified)
|
|
319
|
-
...deviceId && { deviceId: { exact: deviceId } }
|
|
320
|
-
};
|
|
321
|
-
const constraints = {
|
|
322
|
-
audio,
|
|
323
|
-
video: false
|
|
324
|
-
};
|
|
325
|
-
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
326
|
-
setStream(newStream);
|
|
327
|
-
setHasPermission(true);
|
|
328
|
-
await enumerateDevices();
|
|
329
|
-
} catch (err) {
|
|
330
|
-
console.error("Failed to access microphone:", err);
|
|
331
|
-
setError(
|
|
332
|
-
err instanceof Error ? err : new Error("Failed to access microphone")
|
|
333
|
-
);
|
|
334
|
-
setHasPermission(false);
|
|
335
|
-
} finally {
|
|
336
|
-
setIsLoading(false);
|
|
337
|
-
}
|
|
338
|
-
}, [stream, enumerateDevices]);
|
|
322
|
+
},
|
|
323
|
+
[stream, enumerateDevices]
|
|
324
|
+
);
|
|
339
325
|
const stopStream = useCallback2(() => {
|
|
340
326
|
if (stream) {
|
|
341
327
|
stream.getTracks().forEach((track) => track.stop());
|
|
@@ -366,10 +352,7 @@ function useMicrophoneAccess() {
|
|
|
366
352
|
import { useEffect as useEffect3, useState as useState3, useRef as useRef2 } from "react";
|
|
367
353
|
import { Meter, getContext as getContext2, connect } from "tone";
|
|
368
354
|
function useMicrophoneLevel(stream, options = {}) {
|
|
369
|
-
const {
|
|
370
|
-
updateRate = 60,
|
|
371
|
-
smoothingTimeConstant = 0.8
|
|
372
|
-
} = options;
|
|
355
|
+
const { updateRate = 60, smoothingTimeConstant = 0.8 } = options;
|
|
373
356
|
const [level, setLevel] = useState3(0);
|
|
374
357
|
const [peakLevel, setPeakLevel] = useState3(0);
|
|
375
358
|
const meterRef = useRef2(null);
|
|
@@ -446,13 +429,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
446
429
|
const [isMonitoring, setIsMonitoring] = useState4(false);
|
|
447
430
|
const [selectedDevice, setSelectedDevice] = useState4(null);
|
|
448
431
|
const [hookError, setHookError] = useState4(null);
|
|
449
|
-
const {
|
|
450
|
-
stream,
|
|
451
|
-
devices,
|
|
452
|
-
hasPermission,
|
|
453
|
-
requestAccess,
|
|
454
|
-
error: micError
|
|
455
|
-
} = useMicrophoneAccess();
|
|
432
|
+
const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();
|
|
456
433
|
const { level, peakLevel } = useMicrophoneLevel(stream);
|
|
457
434
|
const {
|
|
458
435
|
isRecording,
|
|
@@ -468,7 +445,9 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
468
445
|
} = useRecording(stream, recordingOptions);
|
|
469
446
|
const startRecording = useCallback3(async () => {
|
|
470
447
|
if (!selectedTrackId) {
|
|
471
|
-
setHookError(
|
|
448
|
+
setHookError(
|
|
449
|
+
new Error("Cannot start recording: no track selected. Select or create a track first.")
|
|
450
|
+
);
|
|
472
451
|
return;
|
|
473
452
|
}
|
|
474
453
|
try {
|
|
@@ -548,17 +527,20 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
548
527
|
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
549
528
|
}
|
|
550
529
|
}, [requestAccess, audioConstraints]);
|
|
551
|
-
const changeDevice = useCallback3(
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
530
|
+
const changeDevice = useCallback3(
|
|
531
|
+
async (deviceId) => {
|
|
532
|
+
try {
|
|
533
|
+
setHookError(null);
|
|
534
|
+
setSelectedDevice(deviceId);
|
|
535
|
+
await requestAccess(deviceId, audioConstraints);
|
|
536
|
+
await resumeGlobalAudioContext();
|
|
537
|
+
setIsMonitoring(true);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
[requestAccess, audioConstraints]
|
|
543
|
+
);
|
|
562
544
|
return {
|
|
563
545
|
// Recording state
|
|
564
546
|
isRecording,
|
|
@@ -790,7 +772,9 @@ var MeterFill = styled4.div.attrs((props) => ({
|
|
|
790
772
|
position: absolute;
|
|
791
773
|
left: 0;
|
|
792
774
|
top: 0;
|
|
793
|
-
transition:
|
|
775
|
+
transition:
|
|
776
|
+
width 0.05s ease-out,
|
|
777
|
+
background 0.1s ease-out;
|
|
794
778
|
`;
|
|
795
779
|
var PeakIndicator = styled4.div.attrs((props) => ({
|
|
796
780
|
style: {
|
package/dist/index.mjs.map
CHANGED
|
@@ -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 {\n channelCount = 1,\n samplesPerPixel = 1024,\n } = 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(\n './worklet/recording-processor.worklet.js',\n import.meta.url\n ).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(\n rawContext,\n allSamples,\n rawContext.sampleRate,\n channelCount\n );\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(\n channelCount,\n samples.length,\n sampleRate\n );\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 peakArray[i * 2] = Math.floor(min * maxValue);\n peakArray[i * 2 + 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.floor(min * maxValue);\n updated[existingPeaks.length - 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(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(\n err instanceof Error ? err : new Error('Failed to access microphone')\n );\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n }, [stream, enumerateDevices]);\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 {\n updateRate = 60,\n smoothingTimeConstant = 0.8,\n } = 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 {\n stream,\n devices,\n hasPermission,\n requestAccess,\n error: micError,\n } = 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(new Error('Cannot start recording: no track selected. Select or create a track first.'));\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(clip =>\n 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(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 }, [requestAccess, audioConstraints]);\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: width 0.05s ease-out, 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\n ? Math.max(0, Math.min(1, peakLevel))\n : 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\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 peakArray[i * 2] = Math.floor(min * maxValue);\n peakArray[i * 2 + 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.floor(min * maxValue);\n updated[existingPeaks.length - 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 +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 (
|
|
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 +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 (
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveform-playlist/recording",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.3",
|
|
4
4
|
"description": "Audio recording support for waveform-playlist using AudioWorklet",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"typescript": "^5.3.3"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@waveform-playlist/core": "7.1.
|
|
46
|
-
"@waveform-playlist/playout": "7.1.
|
|
47
|
-
"@waveform-playlist/ui-components": "7.1.
|
|
45
|
+
"@waveform-playlist/core": "7.1.3",
|
|
46
|
+
"@waveform-playlist/playout": "7.1.3",
|
|
47
|
+
"@waveform-playlist/ui-components": "7.1.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^18.0.0",
|