@waveform-playlist/recording 11.3.1 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1 -33
- package/dist/index.d.ts +1 -33
- package/dist/index.js +7 -89
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -79
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -211,36 +211,4 @@ interface UseIntegratedRecordingReturn {
|
|
|
211
211
|
}
|
|
212
212
|
declare function useIntegratedRecording(tracks: ClipTrack[], setTracks: (tracks: ClipTrack[]) => void, selectedTrackId: string | null, options?: IntegratedRecordingOptions): UseIntegratedRecordingReturn;
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
* Peak generation for real-time waveform visualization during recording
|
|
216
|
-
* Matches the format used by webaudio-peaks: min/max pairs with bit depth
|
|
217
|
-
*/
|
|
218
|
-
/**
|
|
219
|
-
* Generate peaks from audio samples in standard min/max pair format
|
|
220
|
-
*
|
|
221
|
-
* @param samples - Audio samples to process
|
|
222
|
-
* @param samplesPerPixel - Number of samples to represent in each peak
|
|
223
|
-
* @param bits - Bit depth for peak values (8 or 16)
|
|
224
|
-
* @returns Int8Array or Int16Array of peak values (min/max pairs)
|
|
225
|
-
*/
|
|
226
|
-
declare function generatePeaks(samples: Float32Array, samplesPerPixel: number, bits?: 8 | 16): Int8Array | Int16Array;
|
|
227
|
-
/**
|
|
228
|
-
* Append new peaks to existing peaks array
|
|
229
|
-
* This is used for incremental peak updates during recording
|
|
230
|
-
*/
|
|
231
|
-
declare function appendPeaks(existingPeaks: Int8Array | Int16Array, newSamples: Float32Array, samplesPerPixel: number, totalSamplesProcessed: number, bits?: 8 | 16): Int8Array | Int16Array;
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Utility functions for working with AudioBuffers during recording
|
|
235
|
-
*/
|
|
236
|
-
/**
|
|
237
|
-
* Concatenate multiple Float32Arrays into a single array
|
|
238
|
-
*/
|
|
239
|
-
declare function concatenateAudioData(chunks: Float32Array[]): Float32Array;
|
|
240
|
-
/**
|
|
241
|
-
* Convert channel data to AudioBuffer.
|
|
242
|
-
* Accepts either per-channel Float32Array[] or a single Float32Array (mono, backwards compatible).
|
|
243
|
-
*/
|
|
244
|
-
declare function createAudioBuffer(audioContext: AudioContext, channelData: Float32Array[] | Float32Array, sampleRate: number, channelCount?: number): AudioBuffer;
|
|
245
|
-
|
|
246
|
-
export { type IntegratedRecordingOptions, type MicrophoneDevice, type RecordingData, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, appendPeaks, concatenateAudioData, createAudioBuffer, generatePeaks, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
|
|
214
|
+
export { type IntegratedRecordingOptions, type MicrophoneDevice, type RecordingData, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
|
package/dist/index.d.ts
CHANGED
|
@@ -211,36 +211,4 @@ interface UseIntegratedRecordingReturn {
|
|
|
211
211
|
}
|
|
212
212
|
declare function useIntegratedRecording(tracks: ClipTrack[], setTracks: (tracks: ClipTrack[]) => void, selectedTrackId: string | null, options?: IntegratedRecordingOptions): UseIntegratedRecordingReturn;
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
* Peak generation for real-time waveform visualization during recording
|
|
216
|
-
* Matches the format used by webaudio-peaks: min/max pairs with bit depth
|
|
217
|
-
*/
|
|
218
|
-
/**
|
|
219
|
-
* Generate peaks from audio samples in standard min/max pair format
|
|
220
|
-
*
|
|
221
|
-
* @param samples - Audio samples to process
|
|
222
|
-
* @param samplesPerPixel - Number of samples to represent in each peak
|
|
223
|
-
* @param bits - Bit depth for peak values (8 or 16)
|
|
224
|
-
* @returns Int8Array or Int16Array of peak values (min/max pairs)
|
|
225
|
-
*/
|
|
226
|
-
declare function generatePeaks(samples: Float32Array, samplesPerPixel: number, bits?: 8 | 16): Int8Array | Int16Array;
|
|
227
|
-
/**
|
|
228
|
-
* Append new peaks to existing peaks array
|
|
229
|
-
* This is used for incremental peak updates during recording
|
|
230
|
-
*/
|
|
231
|
-
declare function appendPeaks(existingPeaks: Int8Array | Int16Array, newSamples: Float32Array, samplesPerPixel: number, totalSamplesProcessed: number, bits?: 8 | 16): Int8Array | Int16Array;
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Utility functions for working with AudioBuffers during recording
|
|
235
|
-
*/
|
|
236
|
-
/**
|
|
237
|
-
* Concatenate multiple Float32Arrays into a single array
|
|
238
|
-
*/
|
|
239
|
-
declare function concatenateAudioData(chunks: Float32Array[]): Float32Array;
|
|
240
|
-
/**
|
|
241
|
-
* Convert channel data to AudioBuffer.
|
|
242
|
-
* Accepts either per-channel Float32Array[] or a single Float32Array (mono, backwards compatible).
|
|
243
|
-
*/
|
|
244
|
-
declare function createAudioBuffer(audioContext: AudioContext, channelData: Float32Array[] | Float32Array, sampleRate: number, channelCount?: number): AudioBuffer;
|
|
245
|
-
|
|
246
|
-
export { type IntegratedRecordingOptions, type MicrophoneDevice, type RecordingData, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, appendPeaks, concatenateAudioData, createAudioBuffer, generatePeaks, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
|
|
214
|
+
export { type IntegratedRecordingOptions, type MicrophoneDevice, type RecordingData, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
|
package/dist/index.js
CHANGED
|
@@ -20,10 +20,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
-
appendPeaks: () => appendPeaks,
|
|
24
|
-
concatenateAudioData: () => concatenateAudioData,
|
|
25
|
-
createAudioBuffer: () => createAudioBuffer,
|
|
26
|
-
generatePeaks: () => generatePeaks,
|
|
27
23
|
useIntegratedRecording: () => useIntegratedRecording,
|
|
28
24
|
useMicrophoneAccess: () => useMicrophoneAccess,
|
|
29
25
|
useMicrophoneLevel: () => useMicrophoneLevel,
|
|
@@ -33,81 +29,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
33
29
|
|
|
34
30
|
// src/hooks/useRecording.ts
|
|
35
31
|
var import_react = require("react");
|
|
36
|
-
|
|
37
|
-
// src/utils/audioBufferUtils.ts
|
|
38
|
-
function concatenateAudioData(chunks) {
|
|
39
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
40
|
-
const result = new Float32Array(totalLength);
|
|
41
|
-
let offset = 0;
|
|
42
|
-
for (const chunk of chunks) {
|
|
43
|
-
result.set(chunk, offset);
|
|
44
|
-
offset += chunk.length;
|
|
45
|
-
}
|
|
46
|
-
return result;
|
|
47
|
-
}
|
|
48
|
-
function createAudioBuffer(audioContext, channelData, sampleRate, channelCount = 1) {
|
|
49
|
-
const channels = channelData instanceof Float32Array ? [channelData] : channelData;
|
|
50
|
-
const length = channels[0]?.length ?? 0;
|
|
51
|
-
const buffer = audioContext.createBuffer(channelCount, length, sampleRate);
|
|
52
|
-
for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {
|
|
53
|
-
buffer.copyToChannel(new Float32Array(channels[ch]), ch);
|
|
54
|
-
}
|
|
55
|
-
return buffer;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// src/utils/peaksGenerator.ts
|
|
59
|
-
function generatePeaks(samples, samplesPerPixel, bits = 16) {
|
|
60
|
-
const numPeaks = Math.ceil(samples.length / samplesPerPixel);
|
|
61
|
-
const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);
|
|
62
|
-
const maxValue = 2 ** (bits - 1);
|
|
63
|
-
for (let i = 0; i < numPeaks; i++) {
|
|
64
|
-
const start = i * samplesPerPixel;
|
|
65
|
-
const end = Math.min(start + samplesPerPixel, samples.length);
|
|
66
|
-
let min = 0;
|
|
67
|
-
let max = 0;
|
|
68
|
-
for (let j = start; j < end; j++) {
|
|
69
|
-
const value = samples[j];
|
|
70
|
-
if (value < min) min = value;
|
|
71
|
-
if (value > max) max = value;
|
|
72
|
-
}
|
|
73
|
-
peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));
|
|
74
|
-
peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));
|
|
75
|
-
}
|
|
76
|
-
return peakArray;
|
|
77
|
-
}
|
|
78
|
-
function appendPeaks(existingPeaks, newSamples, samplesPerPixel, totalSamplesProcessed, bits = 16) {
|
|
79
|
-
const maxValue = 2 ** (bits - 1);
|
|
80
|
-
const remainder = totalSamplesProcessed % samplesPerPixel;
|
|
81
|
-
let offset = 0;
|
|
82
|
-
if (remainder > 0 && existingPeaks.length > 0) {
|
|
83
|
-
const samplesToComplete = samplesPerPixel - remainder;
|
|
84
|
-
const endIndex = Math.min(samplesToComplete, newSamples.length);
|
|
85
|
-
let min = existingPeaks[existingPeaks.length - 2] / maxValue;
|
|
86
|
-
let max = existingPeaks[existingPeaks.length - 1] / maxValue;
|
|
87
|
-
for (let i = 0; i < endIndex; i++) {
|
|
88
|
-
const value = newSamples[i];
|
|
89
|
-
if (value < min) min = value;
|
|
90
|
-
if (value > max) max = value;
|
|
91
|
-
}
|
|
92
|
-
const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);
|
|
93
|
-
updated.set(existingPeaks);
|
|
94
|
-
updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));
|
|
95
|
-
updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));
|
|
96
|
-
offset = endIndex;
|
|
97
|
-
const newPeaks2 = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);
|
|
98
|
-
const result2 = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks2.length);
|
|
99
|
-
result2.set(updated);
|
|
100
|
-
result2.set(newPeaks2, updated.length);
|
|
101
|
-
return result2;
|
|
102
|
-
}
|
|
103
|
-
const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);
|
|
104
|
-
const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);
|
|
105
|
-
result.set(existingPeaks);
|
|
106
|
-
result.set(newPeaks, existingPeaks.length);
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// src/hooks/useRecording.ts
|
|
32
|
+
var import_core = require("@waveform-playlist/core");
|
|
111
33
|
var import_playout = require("@waveform-playlist/playout");
|
|
112
34
|
var import_worklets = require("@waveform-playlist/worklets");
|
|
113
35
|
function emptyPeaks(bits) {
|
|
@@ -219,7 +141,7 @@ function useRecording(stream, options = {}) {
|
|
|
219
141
|
for (let ch = 0; ch < channels.length; ch++) {
|
|
220
142
|
const prev = prevPeaks[ch] ?? emptyPeaks(bits);
|
|
221
143
|
updated.push(
|
|
222
|
-
appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)
|
|
144
|
+
(0, import_core.appendPeaks)(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)
|
|
223
145
|
);
|
|
224
146
|
}
|
|
225
147
|
return updated;
|
|
@@ -277,7 +199,7 @@ function useRecording(stream, options = {}) {
|
|
|
277
199
|
const context = (0, import_playout.getGlobalContext)();
|
|
278
200
|
const rawContext = context.rawContext;
|
|
279
201
|
const numChannels = recordedChunksRef.current.length || channelCount;
|
|
280
|
-
const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));
|
|
202
|
+
const channelData = recordedChunksRef.current.map((chunks) => (0, import_core.concatenateAudioData)(chunks));
|
|
281
203
|
const totalSamples = channelData[0]?.length ?? 0;
|
|
282
204
|
if (totalSamples === 0) {
|
|
283
205
|
console.warn("[waveform-playlist] Recording stopped with 0 samples captured \u2014 discarding");
|
|
@@ -288,7 +210,7 @@ function useRecording(stream, options = {}) {
|
|
|
288
210
|
setLevel(0);
|
|
289
211
|
return null;
|
|
290
212
|
}
|
|
291
|
-
const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);
|
|
213
|
+
const buffer = (0, import_core.createAudioBuffer)(rawContext, channelData, rawContext.sampleRate, numChannels);
|
|
292
214
|
setAudioBuffer(buffer);
|
|
293
215
|
setDuration(buffer.duration);
|
|
294
216
|
isRecordingRef.current = false;
|
|
@@ -454,7 +376,7 @@ function useMicrophoneAccess() {
|
|
|
454
376
|
// src/hooks/useMicrophoneLevel.ts
|
|
455
377
|
var import_react3 = require("react");
|
|
456
378
|
var import_playout2 = require("@waveform-playlist/playout");
|
|
457
|
-
var
|
|
379
|
+
var import_core2 = require("@waveform-playlist/core");
|
|
458
380
|
var import_worklets2 = require("@waveform-playlist/worklets");
|
|
459
381
|
var PEAK_DECAY = 0.98;
|
|
460
382
|
function useMicrophoneLevel(stream, options = {}) {
|
|
@@ -515,8 +437,8 @@ function useMicrophoneLevel(stream, options = {}) {
|
|
|
515
437
|
const rmsValues = [];
|
|
516
438
|
for (let ch = 0; ch < peak.length; ch++) {
|
|
517
439
|
smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);
|
|
518
|
-
peakValues.push((0,
|
|
519
|
-
rmsValues.push((0,
|
|
440
|
+
peakValues.push((0, import_core2.gainToNormalized)(smoothed[ch]));
|
|
441
|
+
rmsValues.push((0, import_core2.gainToNormalized)(rms[ch]));
|
|
520
442
|
}
|
|
521
443
|
const mirroredPeaks = peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;
|
|
522
444
|
const mirroredRms = peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;
|
|
@@ -752,10 +674,6 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
752
674
|
}
|
|
753
675
|
// Annotate the CommonJS export names for ESM import in node:
|
|
754
676
|
0 && (module.exports = {
|
|
755
|
-
appendPeaks,
|
|
756
|
-
concatenateAudioData,
|
|
757
|
-
createAudioBuffer,
|
|
758
|
-
generatePeaks,
|
|
759
677
|
useIntegratedRecording,
|
|
760
678
|
useMicrophoneAccess,
|
|
761
679
|
useMicrophoneLevel,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport {\n useRecording,\n useMicrophoneAccess,\n useMicrophoneLevel,\n useIntegratedRecording,\n} from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n UseIntegratedRecordingReturn,\n IntegratedRecordingOptions,\n} from './hooks';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n\n// Utilities\nexport { generatePeaks, appendPeaks } from './utils/peaksGenerator';\nexport { createAudioBuffer, concatenateAudioData } from './utils/audioBufferUtils';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { recordingProcessorUrl } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n\n // Shared duration update loop — starts a rAF loop that updates duration\n // from performance.now(). Used by both startRecording and resumeRecording.\n const startDurationLoop = useCallback(() => {\n const tick = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(tick);\n }\n };\n tick();\n }, []);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getGlobalContext();\n // Load the worklet module directly on the raw AudioContext.\n // Tone.js's addAudioWorkletModule only loads ONE module per context\n // (caches _workletPromise). If meter-processor was loaded first by\n // useMicrophoneLevel, recording-processor is silently skipped.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(recordingProcessorUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels } = event.data as { channels: Float32Array[] };\n\n if (!channels || channels.length === 0) {\n console.warn('[waveform-playlist] Recording worklet sent empty or missing channels data');\n return;\n }\n\n // Accumulate per-channel samples\n for (let ch = 0; ch < channels.length; ch++) {\n if (!recordedChunksRef.current[ch]) {\n console.warn(\n `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`\n );\n recordedChunksRef.current[ch] = [];\n }\n recordedChunksRef.current[ch].push(channels[ch]);\n }\n // Capture sample offset before incrementing — used by peak alignment\n const samplesProcessedBefore = totalSamplesRef.current;\n totalSamplesRef.current += channels[0].length;\n setPeaks((prevPeaks) => {\n // Ensure we have an entry per channel\n const updated: (Int8Array | Int16Array)[] = [];\n for (let ch = 0; ch < channels.length; ch++) {\n const prev = prevPeaks[ch] ?? emptyPeaks(bits);\n updated.push(\n appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)\n );\n }\n return updated;\n });\n\n // Note: VU meter levels come from useMicrophoneLevel (meter-processor worklet)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Connect and start — after state reset and handler setup\n source.connect(workletNode);\n // Do NOT send sampleRate — worklet uses its global sampleRate (always correct)\n workletNode.port.postMessage({ command: 'start', channelCount: streamChannelCount });\n\n // Listen on MediaStreamTrack for mic unplug (MediaStream has no 'ended' event)\n const audioTrack = stream.getAudioTracks()[0] ?? null;\n audioTrackRef.current = audioTrack;\n if (audioTrack) {\n const onEnded = () => {\n console.warn('[waveform-playlist] Audio track ended (mic unplugged or revoked)');\n // Stop recording to clean up worklet, rAF loop, and UI state\n stopRecordingRef.current?.();\n setError(new Error('Microphone disconnected during recording'));\n };\n onTrackEndedRef.current = onEnded;\n audioTrack.addEventListener('ended', onEnded);\n }\n\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n startDurationLoop();\n } catch (err) {\n console.warn('[waveform-playlist] Failed to start recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecordingRef.current) {\n return null;\n }\n\n try {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n audioTrackRef.current = null;\n onTrackEndedRef.current = null;\n }\n\n // Stop the worklet\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during stop:', String(err));\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to stop recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n startDurationLoop();\n }\n }, [isRecording, isPaused, duration, startDurationLoop]);\n\n // Cleanup on unmount — read refs in cleanup, not effect body.\n // Pattern #16 (copy refs in effect body) doesn't apply to [] deps — refs are\n // null at mount and only set later during startRecording.\n useEffect(() => {\n return () => {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n }\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during cleanup:', String(err));\n }\n }\n try {\n workletNodeRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Worklet disconnect during cleanup:', String(err));\n }\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert channel data to AudioBuffer.\n * Accepts either per-channel Float32Array[] or a single Float32Array (mono, backwards compatible).\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n channelData: Float32Array[] | Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n // Backwards compatibility: single Float32Array → wrap as mono\n const channels: Float32Array[] =\n channelData instanceof Float32Array ? [channelData] : channelData;\n\n const length = channels[0]?.length ?? 0;\n const buffer = audioContext.createBuffer(channelCount, length, sampleRate);\n\n for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {\n buffer.copyToChannel(new Float32Array(channels[ch]), ch);\n }\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer (mono convenience)\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, [newSamples], sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, [combined], sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n // Clamp to valid range: Int16 is [-32768, 32767], Int8 is [-128, 127]\n peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(\n async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(err instanceof Error ? err : new Error('Failed to access microphone'));\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n },\n [stream, enumerateDevices]\n );\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state, enumerate devices, and listen for hot-plug changes\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Re-enumerate when devices are plugged in or removed\n navigator.mediaDevices.addEventListener('devicechange', enumerateDevices);\n\n // Cleanup on unmount\n return () => {\n navigator.mediaDevices.removeEventListener('devicechange', enumerateDevices);\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, [enumerateDevices, stream]);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses an AudioWorklet-based meter processor for sample-accurate\n * peak and RMS metering without requestAnimationFrame overhead.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip } from '@waveform-playlist/core';\nimport {\n resumeGlobalAudioContext,\n getGlobalAudioContext,\n getGlobalContext,\n} from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n /** Per-channel peak levels (0-1). Array length matches channelCount. */\n levels: number[];\n /** Per-channel held peak levels (0-1). Array length matches channelCount. */\n peakLevels: number[];\n /** Per-channel RMS levels (0-1). Array length matches channelCount. */\n rmsLevels: number[];\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: (Int8Array | Int16Array)[];\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Capture timeline position when recording starts (not at stop time)\n const recordingStartTimeRef = useRef(0);\n\n // Keep selectedTrackId and currentTime in refs for use in callbacks.\n // Avoids stale closures and prevents 60fps useCallback recreation\n // (currentTime updates at animation-frame rate during playback).\n const selectedTrackIdRef = useRef(selectedTrackId);\n selectedTrackIdRef.current = selectedTrackId;\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n\n // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const {\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n resetPeak,\n error: meterError,\n } = useMicrophoneLevel(stream, {\n channelCount: recordingOptions.channelCount,\n });\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _recordedAudioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n // Reads selectedTrackId from ref to avoid stale closures when\n // auto-create track + start recording happen in the same render cycle\n const startRecording = useCallback(async () => {\n if (!selectedTrackIdRef.current) {\n setHookError(\n new Error('Cannot start recording: no track selected. Select or create a track first.')\n );\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n // Capture timeline position NOW — before recording starts.\n // Using currentTime at stop time would be wrong during overdub\n // (playback advances currentTime while recording).\n recordingStartTimeRef.current = currentTimeRef.current;\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n const trackId = selectedTrackIdRef.current;\n if (buffer && trackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === trackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${trackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.warn(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Use the captured start time (not live currentTime which advances during overdub)\n const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);\n\n // Latency compensation:\n // Two sources of delay between recording start and audible playback:\n // 1. Tone.js lookAhead (~100ms) — Transport schedules audio ahead of real time\n // 2. Output latency — hardware DAC delay before audio reaches speakers\n // The user hears playback delayed by both, so they perform late relative\n // to the timeline. Skip that duration at the start of the recorded audio.\n const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const totalLatency = outputLatency + lookAhead;\n const latencyOffsetSamples = Math.floor(totalLatency * buffer.sampleRate);\n\n // Guard: very short recordings (< latency compensation) would produce negative duration\n const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);\n if (effectiveDuration === 0) {\n console.warn(\n '[waveform-playlist] Recording too short for latency compensation — discarding'\n );\n setHookError(new Error('Recording was too short to save. Try recording for longer.'));\n return;\n }\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: effectiveDuration,\n offsetSamples: latencyOffsetSamples,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [tracks, setTracks, stopRec]);\n\n // Auto-select first device when available, or fallback if selected device was unplugged\n useEffect(() => {\n if (!hasPermission || devices.length === 0) return;\n\n if (selectedDevice === null) {\n // First-time selection\n setSelectedDevice(devices[0].deviceId);\n } else if (!devices.some((d) => d.deviceId === selectedDevice)) {\n // Selected device was removed — fall back to first available\n const fallbackId = devices[0].deviceId;\n setSelectedDevice(fallbackId);\n resetPeak();\n requestAccess(fallbackId, audioConstraints);\n }\n }, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(\n async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n resetPeak();\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [requestAccess, audioConstraints, resetPeak]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n error: hookError || micError || meterError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n"],"mappings":"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","names":["newPeaks","result","error","import_react","import_react","import_playout","import_worklets","import_react","import_playout"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport {\n useRecording,\n useMicrophoneAccess,\n useMicrophoneLevel,\n useIntegratedRecording,\n} from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n UseIntegratedRecordingReturn,\n IntegratedRecordingOptions,\n} from './hooks';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer, appendPeaks } from '@waveform-playlist/core';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { recordingProcessorUrl } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n\n // Shared duration update loop — starts a rAF loop that updates duration\n // from performance.now(). Used by both startRecording and resumeRecording.\n const startDurationLoop = useCallback(() => {\n const tick = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(tick);\n }\n };\n tick();\n }, []);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getGlobalContext();\n // Load the worklet module directly on the raw AudioContext.\n // Tone.js's addAudioWorkletModule only loads ONE module per context\n // (caches _workletPromise). If meter-processor was loaded first by\n // useMicrophoneLevel, recording-processor is silently skipped.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(recordingProcessorUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels } = event.data as { channels: Float32Array[] };\n\n if (!channels || channels.length === 0) {\n console.warn('[waveform-playlist] Recording worklet sent empty or missing channels data');\n return;\n }\n\n // Accumulate per-channel samples\n for (let ch = 0; ch < channels.length; ch++) {\n if (!recordedChunksRef.current[ch]) {\n console.warn(\n `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`\n );\n recordedChunksRef.current[ch] = [];\n }\n recordedChunksRef.current[ch].push(channels[ch]);\n }\n // Capture sample offset before incrementing — used by peak alignment\n const samplesProcessedBefore = totalSamplesRef.current;\n totalSamplesRef.current += channels[0].length;\n setPeaks((prevPeaks) => {\n // Ensure we have an entry per channel\n const updated: (Int8Array | Int16Array)[] = [];\n for (let ch = 0; ch < channels.length; ch++) {\n const prev = prevPeaks[ch] ?? emptyPeaks(bits);\n updated.push(\n appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)\n );\n }\n return updated;\n });\n\n // Note: VU meter levels come from useMicrophoneLevel (meter-processor worklet)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Connect and start — after state reset and handler setup\n source.connect(workletNode);\n // Do NOT send sampleRate — worklet uses its global sampleRate (always correct)\n workletNode.port.postMessage({ command: 'start', channelCount: streamChannelCount });\n\n // Listen on MediaStreamTrack for mic unplug (MediaStream has no 'ended' event)\n const audioTrack = stream.getAudioTracks()[0] ?? null;\n audioTrackRef.current = audioTrack;\n if (audioTrack) {\n const onEnded = () => {\n console.warn('[waveform-playlist] Audio track ended (mic unplugged or revoked)');\n // Stop recording to clean up worklet, rAF loop, and UI state\n stopRecordingRef.current?.();\n setError(new Error('Microphone disconnected during recording'));\n };\n onTrackEndedRef.current = onEnded;\n audioTrack.addEventListener('ended', onEnded);\n }\n\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n startDurationLoop();\n } catch (err) {\n console.warn('[waveform-playlist] Failed to start recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecordingRef.current) {\n return null;\n }\n\n try {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n audioTrackRef.current = null;\n onTrackEndedRef.current = null;\n }\n\n // Stop the worklet\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during stop:', String(err));\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to stop recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n startDurationLoop();\n }\n }, [isRecording, isPaused, duration, startDurationLoop]);\n\n // Cleanup on unmount — read refs in cleanup, not effect body.\n // Pattern #16 (copy refs in effect body) doesn't apply to [] deps — refs are\n // null at mount and only set later during startRecording.\n useEffect(() => {\n return () => {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n }\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during cleanup:', String(err));\n }\n }\n try {\n workletNodeRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Worklet disconnect during cleanup:', String(err));\n }\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(\n async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(err instanceof Error ? err : new Error('Failed to access microphone'));\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n },\n [stream, enumerateDevices]\n );\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state, enumerate devices, and listen for hot-plug changes\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Re-enumerate when devices are plugged in or removed\n navigator.mediaDevices.addEventListener('devicechange', enumerateDevices);\n\n // Cleanup on unmount\n return () => {\n navigator.mediaDevices.removeEventListener('devicechange', enumerateDevices);\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, [enumerateDevices, stream]);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses an AudioWorklet-based meter processor for sample-accurate\n * peak and RMS metering without requestAnimationFrame overhead.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip } from '@waveform-playlist/core';\nimport {\n resumeGlobalAudioContext,\n getGlobalAudioContext,\n getGlobalContext,\n} from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n /** Per-channel peak levels (0-1). Array length matches channelCount. */\n levels: number[];\n /** Per-channel held peak levels (0-1). Array length matches channelCount. */\n peakLevels: number[];\n /** Per-channel RMS levels (0-1). Array length matches channelCount. */\n rmsLevels: number[];\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: (Int8Array | Int16Array)[];\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Capture timeline position when recording starts (not at stop time)\n const recordingStartTimeRef = useRef(0);\n\n // Keep selectedTrackId and currentTime in refs for use in callbacks.\n // Avoids stale closures and prevents 60fps useCallback recreation\n // (currentTime updates at animation-frame rate during playback).\n const selectedTrackIdRef = useRef(selectedTrackId);\n selectedTrackIdRef.current = selectedTrackId;\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n\n // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const {\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n resetPeak,\n error: meterError,\n } = useMicrophoneLevel(stream, {\n channelCount: recordingOptions.channelCount,\n });\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _recordedAudioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n // Reads selectedTrackId from ref to avoid stale closures when\n // auto-create track + start recording happen in the same render cycle\n const startRecording = useCallback(async () => {\n if (!selectedTrackIdRef.current) {\n setHookError(\n new Error('Cannot start recording: no track selected. Select or create a track first.')\n );\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n // Capture timeline position NOW — before recording starts.\n // Using currentTime at stop time would be wrong during overdub\n // (playback advances currentTime while recording).\n recordingStartTimeRef.current = currentTimeRef.current;\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n const trackId = selectedTrackIdRef.current;\n if (buffer && trackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === trackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${trackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.warn(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Use the captured start time (not live currentTime which advances during overdub)\n const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);\n\n // Latency compensation:\n // Two sources of delay between recording start and audible playback:\n // 1. Tone.js lookAhead (~100ms) — Transport schedules audio ahead of real time\n // 2. Output latency — hardware DAC delay before audio reaches speakers\n // The user hears playback delayed by both, so they perform late relative\n // to the timeline. Skip that duration at the start of the recorded audio.\n const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const totalLatency = outputLatency + lookAhead;\n const latencyOffsetSamples = Math.floor(totalLatency * buffer.sampleRate);\n\n // Guard: very short recordings (< latency compensation) would produce negative duration\n const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);\n if (effectiveDuration === 0) {\n console.warn(\n '[waveform-playlist] Recording too short for latency compensation — discarding'\n );\n setHookError(new Error('Recording was too short to save. Try recording for longer.'));\n return;\n }\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: effectiveDuration,\n offsetSamples: latencyOffsetSamples,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [tracks, setTracks, stopRec]);\n\n // Auto-select first device when available, or fallback if selected device was unplugged\n useEffect(() => {\n if (!hasPermission || devices.length === 0) return;\n\n if (selectedDevice === null) {\n // First-time selection\n setSelectedDevice(devices[0].deviceId);\n } else if (!devices.some((d) => d.deviceId === selectedDevice)) {\n // Selected device was removed — fall back to first available\n const fallbackId = devices[0].deviceId;\n setSelectedDevice(fallbackId);\n resetPeak();\n requestAccess(fallbackId, audioConstraints);\n }\n }, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(\n async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n resetPeak();\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [requestAccess, audioConstraints, resetPeak]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n error: hookError || micError || meterError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n"],"mappings":"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","names":["error","import_react","import_react","import_playout","import_core","import_worklets","import_react","import_playout"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,80 +1,6 @@
|
|
|
1
1
|
// src/hooks/useRecording.ts
|
|
2
2
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
3
|
-
|
|
4
|
-
// src/utils/audioBufferUtils.ts
|
|
5
|
-
function concatenateAudioData(chunks) {
|
|
6
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
7
|
-
const result = new Float32Array(totalLength);
|
|
8
|
-
let offset = 0;
|
|
9
|
-
for (const chunk of chunks) {
|
|
10
|
-
result.set(chunk, offset);
|
|
11
|
-
offset += chunk.length;
|
|
12
|
-
}
|
|
13
|
-
return result;
|
|
14
|
-
}
|
|
15
|
-
function createAudioBuffer(audioContext, channelData, sampleRate, channelCount = 1) {
|
|
16
|
-
const channels = channelData instanceof Float32Array ? [channelData] : channelData;
|
|
17
|
-
const length = channels[0]?.length ?? 0;
|
|
18
|
-
const buffer = audioContext.createBuffer(channelCount, length, sampleRate);
|
|
19
|
-
for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {
|
|
20
|
-
buffer.copyToChannel(new Float32Array(channels[ch]), ch);
|
|
21
|
-
}
|
|
22
|
-
return buffer;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// src/utils/peaksGenerator.ts
|
|
26
|
-
function generatePeaks(samples, samplesPerPixel, bits = 16) {
|
|
27
|
-
const numPeaks = Math.ceil(samples.length / samplesPerPixel);
|
|
28
|
-
const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);
|
|
29
|
-
const maxValue = 2 ** (bits - 1);
|
|
30
|
-
for (let i = 0; i < numPeaks; i++) {
|
|
31
|
-
const start = i * samplesPerPixel;
|
|
32
|
-
const end = Math.min(start + samplesPerPixel, samples.length);
|
|
33
|
-
let min = 0;
|
|
34
|
-
let max = 0;
|
|
35
|
-
for (let j = start; j < end; j++) {
|
|
36
|
-
const value = samples[j];
|
|
37
|
-
if (value < min) min = value;
|
|
38
|
-
if (value > max) max = value;
|
|
39
|
-
}
|
|
40
|
-
peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));
|
|
41
|
-
peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));
|
|
42
|
-
}
|
|
43
|
-
return peakArray;
|
|
44
|
-
}
|
|
45
|
-
function appendPeaks(existingPeaks, newSamples, samplesPerPixel, totalSamplesProcessed, bits = 16) {
|
|
46
|
-
const maxValue = 2 ** (bits - 1);
|
|
47
|
-
const remainder = totalSamplesProcessed % samplesPerPixel;
|
|
48
|
-
let offset = 0;
|
|
49
|
-
if (remainder > 0 && existingPeaks.length > 0) {
|
|
50
|
-
const samplesToComplete = samplesPerPixel - remainder;
|
|
51
|
-
const endIndex = Math.min(samplesToComplete, newSamples.length);
|
|
52
|
-
let min = existingPeaks[existingPeaks.length - 2] / maxValue;
|
|
53
|
-
let max = existingPeaks[existingPeaks.length - 1] / maxValue;
|
|
54
|
-
for (let i = 0; i < endIndex; i++) {
|
|
55
|
-
const value = newSamples[i];
|
|
56
|
-
if (value < min) min = value;
|
|
57
|
-
if (value > max) max = value;
|
|
58
|
-
}
|
|
59
|
-
const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);
|
|
60
|
-
updated.set(existingPeaks);
|
|
61
|
-
updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));
|
|
62
|
-
updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));
|
|
63
|
-
offset = endIndex;
|
|
64
|
-
const newPeaks2 = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);
|
|
65
|
-
const result2 = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks2.length);
|
|
66
|
-
result2.set(updated);
|
|
67
|
-
result2.set(newPeaks2, updated.length);
|
|
68
|
-
return result2;
|
|
69
|
-
}
|
|
70
|
-
const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);
|
|
71
|
-
const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);
|
|
72
|
-
result.set(existingPeaks);
|
|
73
|
-
result.set(newPeaks, existingPeaks.length);
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// src/hooks/useRecording.ts
|
|
3
|
+
import { concatenateAudioData, createAudioBuffer, appendPeaks } from "@waveform-playlist/core";
|
|
78
4
|
import { getGlobalContext } from "@waveform-playlist/playout";
|
|
79
5
|
import { recordingProcessorUrl } from "@waveform-playlist/worklets";
|
|
80
6
|
function emptyPeaks(bits) {
|
|
@@ -722,10 +648,6 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
722
648
|
};
|
|
723
649
|
}
|
|
724
650
|
export {
|
|
725
|
-
appendPeaks,
|
|
726
|
-
concatenateAudioData,
|
|
727
|
-
createAudioBuffer,
|
|
728
|
-
generatePeaks,
|
|
729
651
|
useIntegratedRecording,
|
|
730
652
|
useMicrophoneAccess,
|
|
731
653
|
useMicrophoneLevel,
|
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"],"sourcesContent":["/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { recordingProcessorUrl } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n\n // Shared duration update loop — starts a rAF loop that updates duration\n // from performance.now(). Used by both startRecording and resumeRecording.\n const startDurationLoop = useCallback(() => {\n const tick = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(tick);\n }\n };\n tick();\n }, []);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getGlobalContext();\n // Load the worklet module directly on the raw AudioContext.\n // Tone.js's addAudioWorkletModule only loads ONE module per context\n // (caches _workletPromise). If meter-processor was loaded first by\n // useMicrophoneLevel, recording-processor is silently skipped.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(recordingProcessorUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels } = event.data as { channels: Float32Array[] };\n\n if (!channels || channels.length === 0) {\n console.warn('[waveform-playlist] Recording worklet sent empty or missing channels data');\n return;\n }\n\n // Accumulate per-channel samples\n for (let ch = 0; ch < channels.length; ch++) {\n if (!recordedChunksRef.current[ch]) {\n console.warn(\n `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`\n );\n recordedChunksRef.current[ch] = [];\n }\n recordedChunksRef.current[ch].push(channels[ch]);\n }\n // Capture sample offset before incrementing — used by peak alignment\n const samplesProcessedBefore = totalSamplesRef.current;\n totalSamplesRef.current += channels[0].length;\n setPeaks((prevPeaks) => {\n // Ensure we have an entry per channel\n const updated: (Int8Array | Int16Array)[] = [];\n for (let ch = 0; ch < channels.length; ch++) {\n const prev = prevPeaks[ch] ?? emptyPeaks(bits);\n updated.push(\n appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)\n );\n }\n return updated;\n });\n\n // Note: VU meter levels come from useMicrophoneLevel (meter-processor worklet)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Connect and start — after state reset and handler setup\n source.connect(workletNode);\n // Do NOT send sampleRate — worklet uses its global sampleRate (always correct)\n workletNode.port.postMessage({ command: 'start', channelCount: streamChannelCount });\n\n // Listen on MediaStreamTrack for mic unplug (MediaStream has no 'ended' event)\n const audioTrack = stream.getAudioTracks()[0] ?? null;\n audioTrackRef.current = audioTrack;\n if (audioTrack) {\n const onEnded = () => {\n console.warn('[waveform-playlist] Audio track ended (mic unplugged or revoked)');\n // Stop recording to clean up worklet, rAF loop, and UI state\n stopRecordingRef.current?.();\n setError(new Error('Microphone disconnected during recording'));\n };\n onTrackEndedRef.current = onEnded;\n audioTrack.addEventListener('ended', onEnded);\n }\n\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n startDurationLoop();\n } catch (err) {\n console.warn('[waveform-playlist] Failed to start recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecordingRef.current) {\n return null;\n }\n\n try {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n audioTrackRef.current = null;\n onTrackEndedRef.current = null;\n }\n\n // Stop the worklet\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during stop:', String(err));\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to stop recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n startDurationLoop();\n }\n }, [isRecording, isPaused, duration, startDurationLoop]);\n\n // Cleanup on unmount — read refs in cleanup, not effect body.\n // Pattern #16 (copy refs in effect body) doesn't apply to [] deps — refs are\n // null at mount and only set later during startRecording.\n useEffect(() => {\n return () => {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n }\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during cleanup:', String(err));\n }\n }\n try {\n workletNodeRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Worklet disconnect during cleanup:', String(err));\n }\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert channel data to AudioBuffer.\n * Accepts either per-channel Float32Array[] or a single Float32Array (mono, backwards compatible).\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n channelData: Float32Array[] | Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n // Backwards compatibility: single Float32Array → wrap as mono\n const channels: Float32Array[] =\n channelData instanceof Float32Array ? [channelData] : channelData;\n\n const length = channels[0]?.length ?? 0;\n const buffer = audioContext.createBuffer(channelCount, length, sampleRate);\n\n for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {\n buffer.copyToChannel(new Float32Array(channels[ch]), ch);\n }\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer (mono convenience)\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, [newSamples], sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, [combined], sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n // Clamp to valid range: Int16 is [-32768, 32767], Int8 is [-128, 127]\n peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));\n updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(\n async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(err instanceof Error ? err : new Error('Failed to access microphone'));\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n },\n [stream, enumerateDevices]\n );\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state, enumerate devices, and listen for hot-plug changes\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Re-enumerate when devices are plugged in or removed\n navigator.mediaDevices.addEventListener('devicechange', enumerateDevices);\n\n // Cleanup on unmount\n return () => {\n navigator.mediaDevices.removeEventListener('devicechange', enumerateDevices);\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, [enumerateDevices, stream]);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses an AudioWorklet-based meter processor for sample-accurate\n * peak and RMS metering without requestAnimationFrame overhead.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip } from '@waveform-playlist/core';\nimport {\n resumeGlobalAudioContext,\n getGlobalAudioContext,\n getGlobalContext,\n} from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n /** Per-channel peak levels (0-1). Array length matches channelCount. */\n levels: number[];\n /** Per-channel held peak levels (0-1). Array length matches channelCount. */\n peakLevels: number[];\n /** Per-channel RMS levels (0-1). Array length matches channelCount. */\n rmsLevels: number[];\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: (Int8Array | Int16Array)[];\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Capture timeline position when recording starts (not at stop time)\n const recordingStartTimeRef = useRef(0);\n\n // Keep selectedTrackId and currentTime in refs for use in callbacks.\n // Avoids stale closures and prevents 60fps useCallback recreation\n // (currentTime updates at animation-frame rate during playback).\n const selectedTrackIdRef = useRef(selectedTrackId);\n selectedTrackIdRef.current = selectedTrackId;\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n\n // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const {\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n resetPeak,\n error: meterError,\n } = useMicrophoneLevel(stream, {\n channelCount: recordingOptions.channelCount,\n });\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _recordedAudioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n // Reads selectedTrackId from ref to avoid stale closures when\n // auto-create track + start recording happen in the same render cycle\n const startRecording = useCallback(async () => {\n if (!selectedTrackIdRef.current) {\n setHookError(\n new Error('Cannot start recording: no track selected. Select or create a track first.')\n );\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n // Capture timeline position NOW — before recording starts.\n // Using currentTime at stop time would be wrong during overdub\n // (playback advances currentTime while recording).\n recordingStartTimeRef.current = currentTimeRef.current;\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n const trackId = selectedTrackIdRef.current;\n if (buffer && trackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === trackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${trackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.warn(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Use the captured start time (not live currentTime which advances during overdub)\n const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);\n\n // Latency compensation:\n // Two sources of delay between recording start and audible playback:\n // 1. Tone.js lookAhead (~100ms) — Transport schedules audio ahead of real time\n // 2. Output latency — hardware DAC delay before audio reaches speakers\n // The user hears playback delayed by both, so they perform late relative\n // to the timeline. Skip that duration at the start of the recorded audio.\n const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const totalLatency = outputLatency + lookAhead;\n const latencyOffsetSamples = Math.floor(totalLatency * buffer.sampleRate);\n\n // Guard: very short recordings (< latency compensation) would produce negative duration\n const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);\n if (effectiveDuration === 0) {\n console.warn(\n '[waveform-playlist] Recording too short for latency compensation — discarding'\n );\n setHookError(new Error('Recording was too short to save. Try recording for longer.'));\n return;\n }\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: effectiveDuration,\n offsetSamples: latencyOffsetSamples,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [tracks, setTracks, stopRec]);\n\n // Auto-select first device when available, or fallback if selected device was unplugged\n useEffect(() => {\n if (!hasPermission || devices.length === 0) return;\n\n if (selectedDevice === null) {\n // First-time selection\n setSelectedDevice(devices[0].deviceId);\n } else if (!devices.some((d) => d.deviceId === selectedDevice)) {\n // Selected device was removed — fall back to first available\n const fallbackId = devices[0].deviceId;\n setSelectedDevice(fallbackId);\n resetPeak();\n requestAccess(fallbackId, audioConstraints);\n }\n }, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(\n async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n resetPeak();\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [requestAccess, audioConstraints, resetPeak]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n error: hookError || micError || meterError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n"],"mappings":"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","names":["newPeaks","result","error","useState","useEffect","useCallback","useEffect","useState","useRef","useCallback","getGlobalContext","useState","useCallback","useEffect","useRef","getGlobalContext","useState","useRef","useCallback","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer, appendPeaks } from '@waveform-playlist/core';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { recordingProcessorUrl } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n\n // Shared duration update loop — starts a rAF loop that updates duration\n // from performance.now(). Used by both startRecording and resumeRecording.\n const startDurationLoop = useCallback(() => {\n const tick = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(tick);\n }\n };\n tick();\n }, []);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getGlobalContext();\n // Load the worklet module directly on the raw AudioContext.\n // Tone.js's addAudioWorkletModule only loads ONE module per context\n // (caches _workletPromise). If meter-processor was loaded first by\n // useMicrophoneLevel, recording-processor is silently skipped.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(recordingProcessorUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels } = event.data as { channels: Float32Array[] };\n\n if (!channels || channels.length === 0) {\n console.warn('[waveform-playlist] Recording worklet sent empty or missing channels data');\n return;\n }\n\n // Accumulate per-channel samples\n for (let ch = 0; ch < channels.length; ch++) {\n if (!recordedChunksRef.current[ch]) {\n console.warn(\n `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`\n );\n recordedChunksRef.current[ch] = [];\n }\n recordedChunksRef.current[ch].push(channels[ch]);\n }\n // Capture sample offset before incrementing — used by peak alignment\n const samplesProcessedBefore = totalSamplesRef.current;\n totalSamplesRef.current += channels[0].length;\n setPeaks((prevPeaks) => {\n // Ensure we have an entry per channel\n const updated: (Int8Array | Int16Array)[] = [];\n for (let ch = 0; ch < channels.length; ch++) {\n const prev = prevPeaks[ch] ?? emptyPeaks(bits);\n updated.push(\n appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)\n );\n }\n return updated;\n });\n\n // Note: VU meter levels come from useMicrophoneLevel (meter-processor worklet)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Connect and start — after state reset and handler setup\n source.connect(workletNode);\n // Do NOT send sampleRate — worklet uses its global sampleRate (always correct)\n workletNode.port.postMessage({ command: 'start', channelCount: streamChannelCount });\n\n // Listen on MediaStreamTrack for mic unplug (MediaStream has no 'ended' event)\n const audioTrack = stream.getAudioTracks()[0] ?? null;\n audioTrackRef.current = audioTrack;\n if (audioTrack) {\n const onEnded = () => {\n console.warn('[waveform-playlist] Audio track ended (mic unplugged or revoked)');\n // Stop recording to clean up worklet, rAF loop, and UI state\n stopRecordingRef.current?.();\n setError(new Error('Microphone disconnected during recording'));\n };\n onTrackEndedRef.current = onEnded;\n audioTrack.addEventListener('ended', onEnded);\n }\n\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n startDurationLoop();\n } catch (err) {\n console.warn('[waveform-playlist] Failed to start recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecordingRef.current) {\n return null;\n }\n\n try {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n audioTrackRef.current = null;\n onTrackEndedRef.current = null;\n }\n\n // Stop the worklet\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during stop:', String(err));\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to stop recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n startDurationLoop();\n }\n }, [isRecording, isPaused, duration, startDurationLoop]);\n\n // Cleanup on unmount — read refs in cleanup, not effect body.\n // Pattern #16 (copy refs in effect body) doesn't apply to [] deps — refs are\n // null at mount and only set later during startRecording.\n useEffect(() => {\n return () => {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n }\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during cleanup:', String(err));\n }\n }\n try {\n workletNodeRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Worklet disconnect during cleanup:', String(err));\n }\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(\n async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(err instanceof Error ? err : new Error('Failed to access microphone'));\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n },\n [stream, enumerateDevices]\n );\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state, enumerate devices, and listen for hot-plug changes\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Re-enumerate when devices are plugged in or removed\n navigator.mediaDevices.addEventListener('devicechange', enumerateDevices);\n\n // Cleanup on unmount\n return () => {\n navigator.mediaDevices.removeEventListener('devicechange', enumerateDevices);\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, [enumerateDevices, stream]);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses an AudioWorklet-based meter processor for sample-accurate\n * peak and RMS metering without requestAnimationFrame overhead.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n // only loads ONE module per context (caches _workletPromise), silently\n // skipping subsequent calls with different URLs.\n const rawCtx = context.rawContext as AudioContext;\n await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip } from '@waveform-playlist/core';\nimport {\n resumeGlobalAudioContext,\n getGlobalAudioContext,\n getGlobalContext,\n} from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n /** Per-channel peak levels (0-1). Array length matches channelCount. */\n levels: number[];\n /** Per-channel held peak levels (0-1). Array length matches channelCount. */\n peakLevels: number[];\n /** Per-channel RMS levels (0-1). Array length matches channelCount. */\n rmsLevels: number[];\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: (Int8Array | Int16Array)[];\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Capture timeline position when recording starts (not at stop time)\n const recordingStartTimeRef = useRef(0);\n\n // Keep selectedTrackId and currentTime in refs for use in callbacks.\n // Avoids stale closures and prevents 60fps useCallback recreation\n // (currentTime updates at animation-frame rate during playback).\n const selectedTrackIdRef = useRef(selectedTrackId);\n selectedTrackIdRef.current = selectedTrackId;\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n\n // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const {\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n resetPeak,\n error: meterError,\n } = useMicrophoneLevel(stream, {\n channelCount: recordingOptions.channelCount,\n });\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _recordedAudioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n // Reads selectedTrackId from ref to avoid stale closures when\n // auto-create track + start recording happen in the same render cycle\n const startRecording = useCallback(async () => {\n if (!selectedTrackIdRef.current) {\n setHookError(\n new Error('Cannot start recording: no track selected. Select or create a track first.')\n );\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n // Capture timeline position NOW — before recording starts.\n // Using currentTime at stop time would be wrong during overdub\n // (playback advances currentTime while recording).\n recordingStartTimeRef.current = currentTimeRef.current;\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n const trackId = selectedTrackIdRef.current;\n if (buffer && trackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === trackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${trackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.warn(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Use the captured start time (not live currentTime which advances during overdub)\n const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);\n\n // Latency compensation:\n // Two sources of delay between recording start and audible playback:\n // 1. Tone.js lookAhead (~100ms) — Transport schedules audio ahead of real time\n // 2. Output latency — hardware DAC delay before audio reaches speakers\n // The user hears playback delayed by both, so they perform late relative\n // to the timeline. Skip that duration at the start of the recorded audio.\n const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const totalLatency = outputLatency + lookAhead;\n const latencyOffsetSamples = Math.floor(totalLatency * buffer.sampleRate);\n\n // Guard: very short recordings (< latency compensation) would produce negative duration\n const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);\n if (effectiveDuration === 0) {\n console.warn(\n '[waveform-playlist] Recording too short for latency compensation — discarding'\n );\n setHookError(new Error('Recording was too short to save. Try recording for longer.'));\n return;\n }\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: effectiveDuration,\n offsetSamples: latencyOffsetSamples,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [tracks, setTracks, stopRec]);\n\n // Auto-select first device when available, or fallback if selected device was unplugged\n useEffect(() => {\n if (!hasPermission || devices.length === 0) return;\n\n if (selectedDevice === null) {\n // First-time selection\n setSelectedDevice(devices[0].deviceId);\n } else if (!devices.some((d) => d.deviceId === selectedDevice)) {\n // Selected device was removed — fall back to first available\n const fallbackId = devices[0].deviceId;\n setSelectedDevice(fallbackId);\n resetPeak();\n requestAccess(fallbackId, audioConstraints);\n }\n }, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(\n async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n resetPeak();\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [requestAccess, audioConstraints, resetPeak]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n error: hookError || micError || meterError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n"],"mappings":"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","names":["error","useState","useEffect","useCallback","useEffect","useState","useRef","useCallback","getGlobalContext","useState","useCallback","useEffect","useRef","getGlobalContext","useState","useRef","useCallback","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveform-playlist/recording",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
4
4
|
"description": "Audio recording support for waveform-playlist using AudioWorklet",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"vitest": "^3.0.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@waveform-playlist/core": "
|
|
47
|
-
"@waveform-playlist/playout": "
|
|
48
|
-
"@waveform-playlist/
|
|
49
|
-
"@waveform-playlist/
|
|
46
|
+
"@waveform-playlist/core": "12.0.0",
|
|
47
|
+
"@waveform-playlist/playout": "12.0.0",
|
|
48
|
+
"@waveform-playlist/worklets": "12.0.0",
|
|
49
|
+
"@waveform-playlist/ui-components": "12.0.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "^18.0.0",
|