@waveform-playlist/recording 12.0.1 → 12.1.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.js +78 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +78 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -5
package/dist/index.js
CHANGED
|
@@ -57,9 +57,11 @@ function useRecording(stream, options = {}) {
|
|
|
57
57
|
const audioTrackRef = (0, import_react.useRef)(null);
|
|
58
58
|
const onTrackEndedRef = (0, import_react.useRef)(null);
|
|
59
59
|
const stopRecordingRef = (0, import_react.useRef)(null);
|
|
60
|
+
const stopAckResolveRef = (0, import_react.useRef)(null);
|
|
61
|
+
const isStoppingRef = (0, import_react.useRef)(false);
|
|
60
62
|
const startDurationLoop = (0, import_react.useCallback)(() => {
|
|
61
63
|
const tick = () => {
|
|
62
|
-
if (isRecordingRef.current && !isPausedRef.current) {
|
|
64
|
+
if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {
|
|
63
65
|
const elapsed = (performance.now() - startTimeRef.current) / 1e3;
|
|
64
66
|
setDuration(elapsed);
|
|
65
67
|
animationFrameRef.current = requestAnimationFrame(tick);
|
|
@@ -120,32 +122,36 @@ function useRecording(stream, options = {}) {
|
|
|
120
122
|
setLevel(0);
|
|
121
123
|
setPeakLevel(0);
|
|
122
124
|
workletNode.port.onmessage = (event) => {
|
|
123
|
-
const { channels } = event.data;
|
|
124
|
-
if (
|
|
125
|
-
console.warn("[waveform-playlist] Recording worklet sent empty or missing channels data");
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
for (let ch = 0; ch < channels.length; ch++) {
|
|
129
|
-
if (!recordedChunksRef.current[ch]) {
|
|
130
|
-
console.warn(
|
|
131
|
-
`[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`
|
|
132
|
-
);
|
|
133
|
-
recordedChunksRef.current[ch] = [];
|
|
134
|
-
}
|
|
135
|
-
recordedChunksRef.current[ch].push(channels[ch]);
|
|
136
|
-
}
|
|
137
|
-
const samplesProcessedBefore = totalSamplesRef.current;
|
|
138
|
-
totalSamplesRef.current += channels[0].length;
|
|
139
|
-
setPeaks((prevPeaks) => {
|
|
140
|
-
const updated = [];
|
|
125
|
+
const { channels, done } = event.data;
|
|
126
|
+
if (channels && channels.length > 0 && channels[0] && channels[0].length > 0) {
|
|
141
127
|
for (let ch = 0; ch < channels.length; ch++) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
128
|
+
if (!recordedChunksRef.current[ch]) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`
|
|
131
|
+
);
|
|
132
|
+
recordedChunksRef.current[ch] = [];
|
|
133
|
+
}
|
|
134
|
+
recordedChunksRef.current[ch].push(channels[ch]);
|
|
146
135
|
}
|
|
147
|
-
|
|
148
|
-
|
|
136
|
+
const samplesProcessedBefore = totalSamplesRef.current;
|
|
137
|
+
totalSamplesRef.current += channels[0].length;
|
|
138
|
+
if (stopAckResolveRef.current === null) {
|
|
139
|
+
setPeaks((prevPeaks) => {
|
|
140
|
+
const updated = [];
|
|
141
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
142
|
+
const prev = prevPeaks[ch] ?? emptyPeaks(bits);
|
|
143
|
+
updated.push(
|
|
144
|
+
(0, import_core.appendPeaks)(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return updated;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (done) {
|
|
152
|
+
stopAckResolveRef.current?.();
|
|
153
|
+
stopAckResolveRef.current = null;
|
|
154
|
+
}
|
|
149
155
|
};
|
|
150
156
|
source.connect(workletNode);
|
|
151
157
|
workletNode.port.postMessage({ command: "start", channelCount: streamChannelCount });
|
|
@@ -176,25 +182,49 @@ function useRecording(stream, options = {}) {
|
|
|
176
182
|
return null;
|
|
177
183
|
}
|
|
178
184
|
try {
|
|
185
|
+
isStoppingRef.current = true;
|
|
186
|
+
if (animationFrameRef.current !== null) {
|
|
187
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
188
|
+
animationFrameRef.current = null;
|
|
189
|
+
}
|
|
179
190
|
if (audioTrackRef.current && onTrackEndedRef.current) {
|
|
180
191
|
audioTrackRef.current.removeEventListener("ended", onTrackEndedRef.current);
|
|
181
192
|
audioTrackRef.current = null;
|
|
182
193
|
onTrackEndedRef.current = null;
|
|
183
194
|
}
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
const node = workletNodeRef.current;
|
|
196
|
+
if (node) {
|
|
197
|
+
const stopAck = new Promise((resolve) => {
|
|
198
|
+
stopAckResolveRef.current = resolve;
|
|
199
|
+
});
|
|
200
|
+
let timeoutId;
|
|
201
|
+
const timeout = new Promise((resolve) => {
|
|
202
|
+
timeoutId = setTimeout(resolve, 1e3);
|
|
203
|
+
});
|
|
204
|
+
node.port.postMessage({ command: "stop" });
|
|
205
|
+
await Promise.race([stopAck, timeout]);
|
|
206
|
+
clearTimeout(timeoutId);
|
|
207
|
+
stopAckResolveRef.current = null;
|
|
208
|
+
let lastSamples = -1;
|
|
209
|
+
let stable = 0;
|
|
210
|
+
for (let i = 0; i < 50; i++) {
|
|
211
|
+
if (totalSamplesRef.current === lastSamples) {
|
|
212
|
+
if (++stable >= 3) break;
|
|
213
|
+
} else {
|
|
214
|
+
stable = 0;
|
|
215
|
+
lastSamples = totalSamplesRef.current;
|
|
216
|
+
}
|
|
217
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
218
|
+
}
|
|
219
|
+
node.port.onmessage = null;
|
|
186
220
|
if (mediaStreamSourceRef.current) {
|
|
187
221
|
try {
|
|
188
|
-
mediaStreamSourceRef.current.disconnect(
|
|
222
|
+
mediaStreamSourceRef.current.disconnect(node);
|
|
189
223
|
} catch (err) {
|
|
190
224
|
console.warn("[waveform-playlist] Source disconnect during stop:", String(err));
|
|
191
225
|
}
|
|
192
226
|
}
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
if (animationFrameRef.current !== null) {
|
|
196
|
-
cancelAnimationFrame(animationFrameRef.current);
|
|
197
|
-
animationFrameRef.current = null;
|
|
227
|
+
node.disconnect();
|
|
198
228
|
}
|
|
199
229
|
const context = (0, import_playout.getGlobalContext)();
|
|
200
230
|
const rawContext = context.rawContext;
|
|
@@ -203,31 +233,29 @@ function useRecording(stream, options = {}) {
|
|
|
203
233
|
const totalSamples = channelData[0]?.length ?? 0;
|
|
204
234
|
if (totalSamples === 0) {
|
|
205
235
|
console.warn("[waveform-playlist] Recording stopped with 0 samples captured \u2014 discarding");
|
|
206
|
-
isRecordingRef.current = false;
|
|
207
|
-
isPausedRef.current = false;
|
|
208
|
-
setIsRecording(false);
|
|
209
|
-
setIsPaused(false);
|
|
210
|
-
setLevel(0);
|
|
211
236
|
return null;
|
|
212
237
|
}
|
|
213
238
|
const buffer = (0, import_core.createAudioBuffer)(rawContext, channelData, rawContext.sampleRate, numChannels);
|
|
214
239
|
setAudioBuffer(buffer);
|
|
215
240
|
setDuration(buffer.duration);
|
|
216
|
-
isRecordingRef.current = false;
|
|
217
|
-
isPausedRef.current = false;
|
|
218
|
-
setIsRecording(false);
|
|
219
|
-
setIsPaused(false);
|
|
220
|
-
setLevel(0);
|
|
221
241
|
return buffer;
|
|
222
242
|
} catch (err) {
|
|
223
243
|
console.warn("[waveform-playlist] Failed to stop recording:", String(err));
|
|
224
244
|
setError(err instanceof Error ? err : new Error("Failed to stop recording"));
|
|
225
245
|
return null;
|
|
246
|
+
} finally {
|
|
247
|
+
isRecordingRef.current = false;
|
|
248
|
+
isPausedRef.current = false;
|
|
249
|
+
isStoppingRef.current = false;
|
|
250
|
+
setIsRecording(false);
|
|
251
|
+
setIsPaused(false);
|
|
252
|
+
setLevel(0);
|
|
226
253
|
}
|
|
227
254
|
}, [channelCount]);
|
|
228
255
|
stopRecordingRef.current = stopRecording;
|
|
229
256
|
const pauseRecording = (0, import_react.useCallback)(() => {
|
|
230
257
|
if (isRecording && !isPaused) {
|
|
258
|
+
workletNodeRef.current?.port.postMessage({ command: "pause" });
|
|
231
259
|
if (animationFrameRef.current !== null) {
|
|
232
260
|
cancelAnimationFrame(animationFrameRef.current);
|
|
233
261
|
animationFrameRef.current = null;
|
|
@@ -238,6 +266,7 @@ function useRecording(stream, options = {}) {
|
|
|
238
266
|
}, [isRecording, isPaused]);
|
|
239
267
|
const resumeRecording = (0, import_react.useCallback)(() => {
|
|
240
268
|
if (isRecording && isPaused) {
|
|
269
|
+
workletNodeRef.current?.port.postMessage({ command: "resume" });
|
|
241
270
|
isPausedRef.current = false;
|
|
242
271
|
setIsPaused(false);
|
|
243
272
|
startTimeRef.current = performance.now() - duration * 1e3;
|
|
@@ -489,6 +518,7 @@ function useMicrophoneLevel(stream, options = {}) {
|
|
|
489
518
|
|
|
490
519
|
// src/hooks/useIntegratedRecording.ts
|
|
491
520
|
var import_react4 = require("react");
|
|
521
|
+
var import_core3 = require("@waveform-playlist/core");
|
|
492
522
|
var import_playout3 = require("@waveform-playlist/playout");
|
|
493
523
|
function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}) {
|
|
494
524
|
const { currentTime = 0, audioConstraints, ...recordingOptions } = options;
|
|
@@ -576,8 +606,11 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
576
606
|
const outputLatency = audioContext.outputLatency ?? 0;
|
|
577
607
|
const toneContext = (0, import_playout3.getGlobalContext)();
|
|
578
608
|
const lookAhead = toneContext.lookAhead ?? 0;
|
|
579
|
-
const
|
|
580
|
-
|
|
609
|
+
const latencyOffsetSamples = (0, import_core3.audibleLatencySamples)(
|
|
610
|
+
outputLatency,
|
|
611
|
+
lookAhead,
|
|
612
|
+
buffer.sampleRate
|
|
613
|
+
);
|
|
581
614
|
const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);
|
|
582
615
|
if (effectiveDuration === 0) {
|
|
583
616
|
console.warn(
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport {\n useRecording,\n useMicrophoneAccess,\n useMicrophoneLevel,\n useIntegratedRecording,\n} from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n UseIntegratedRecordingReturn,\n IntegratedRecordingOptions,\n} from './hooks';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer, appendPeaks } from '@waveform-playlist/core';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { addRecordingWorkletModule } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n\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 // Use addRecordingWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addRecordingWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels } = 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 { addMeterWorkletModule, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Use addMeterWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addMeterWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip } 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,mBAAyD;AAEzD,kBAAqE;AACrE,qBAAiC;AACjC,sBAA0C;AAE1C,SAAS,WAAW,MAAsC;AACxD,SAAO,SAAS,IAAI,IAAI,UAAU,CAAC,IAAI,IAAI,WAAW,CAAC;AACzD;AAEO,SAAS,aACd,QACA,UAA4B,CAAC,GACT;AACpB,QAAM,EAAE,eAAe,GAAG,kBAAkB,MAAM,OAAO,GAAG,IAAI;AAGhE,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAS,KAAK;AACpD,QAAM,CAAC,UAAU,WAAW,QAAI,uBAAS,KAAK;AAC9C,QAAM,CAAC,UAAU,WAAW,QAAI,uBAAS,CAAC;AAE1C,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAqC,CAAC,WAAW,IAAI,CAAC,CAAC;AACjF,QAAM,CAAC,aAAa,cAAc,QAAI,uBAA6B,IAAI;AACvE,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAuB,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAS,CAAC;AACpC,QAAM,CAAC,WAAW,YAAY,QAAI,uBAAS,CAAC;AAK5C,QAAM,uBAAmB,qBAAgB,KAAK;AAG9C,QAAM,qBAAiB,qBAAgC,IAAI;AAC3D,QAAM,2BAAuB,qBAA0C,IAAI;AAE3E,QAAM,wBAAoB,qBAAyB,CAAC,CAAC;AACrD,QAAM,sBAAkB,qBAAO,CAAC;AAChC,QAAM,wBAAoB,qBAAsB,IAAI;AACpD,QAAM,mBAAe,qBAAe,CAAC;AACrC,QAAM,qBAAiB,qBAAgB,KAAK;AAC5C,QAAM,kBAAc,qBAAgB,KAAK;AACzC,QAAM,oBAAgB,qBAAgC,IAAI;AAC1D,QAAM,sBAAkB,qBAA4B,IAAI;AACxD,QAAM,uBAAmB,qBAAmD,IAAI;AAIhF,QAAM,wBAAoB,0BAAY,MAAM;AAC1C,UAAM,OAAO,MAAM;AACjB,UAAI,eAAe,WAAW,CAAC,YAAY,SAAS;AAClD,cAAM,WAAW,YAAY,IAAI,IAAI,aAAa,WAAW;AAC7D,oBAAY,OAAO;AACnB,0BAAkB,UAAU,sBAAsB,IAAI;AAAA,MACxD;AAAA,IACF;AACA,SAAK;AAAA,EACP,GAAG,CAAC,CAAC;AAGL,QAAM,kBAAc,0BAAY,YAAY;AAE1C,QAAI,iBAAiB,SAAS;AAC5B;AAAA,IACF;AAEA,QAAI;AACF,YAAM,cAAU,iCAAiB;AAIjC,YAAM,SAAS,QAAQ;AACvB,gBAAM,2CAA0B,CAAC,QAAQ,OAAO,aAAa,UAAU,GAAG,CAAC;AAC3E,uBAAiB,UAAU;AAAA,IAC7B,SAAS,KAAK;AACZ,cAAQ,KAAK,2DAA2D,OAAO,GAAG,CAAC;AACnF,YAAMA,SAAQ,IAAI,MAAM,yCAAyC,OAAO,GAAG,CAAC;AAC5E,YAAMA;AAAA,IACR;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,qBAAiB,0BAAY,YAAY;AAC7C,QAAI,eAAe,QAAS;AAC5B,QAAI,CAAC,QAAQ;AACX,eAAS,IAAI,MAAM,gCAAgC,CAAC;AACpD;AAAA,IACF;AAEA,QAAI;AACF,eAAS,IAAI;AAGb,YAAM,cAAU,iCAAiB;AAGjC,UAAI,QAAQ,UAAU,aAAa;AACjC,cAAM,QAAQ,OAAO;AAAA,MACvB;AAGA,YAAM,YAAY;AAIlB,YAAM,SAAS,QAAQ,wBAAwB,MAAM;AACrD,2BAAqB,UAAU;AAK/B,YAAM,uBAAuB,OAAO,eAAe,EAAE,CAAC,GAAG,YAAY,EAAE;AACvE,UAAI,yBAAyB,QAAW;AACtC,gBAAQ;AAAA,UACN,8EAA8E,YAAY;AAAA,QAC5F;AAAA,MACF;AACA,YAAM,qBAAqB,wBAAwB;AAEnD,YAAM,cAAc,QAAQ,uBAAuB,uBAAuB;AAAA,QACxE,cAAc;AAAA,QACd,kBAAkB;AAAA,MACpB,CAAC;AACD,qBAAe,UAAU;AAEzB,kBAAY,mBAAmB,CAAC,UAAU;AACxC,gBAAQ,KAAK,0DAA0D,OAAO,KAAK,CAAC;AACpF,iBAAS,IAAI,MAAM,0CAA0C,CAAC;AAAA,MAChE;AAIA,wBAAkB,UAAU,MAAM,KAAK,EAAE,QAAQ,mBAAmB,GAAG,MAAM,CAAC,CAAC;AAC/E,sBAAgB,UAAU;AAC1B,eAAS,MAAM,KAAK,EAAE,QAAQ,mBAAmB,GAAG,MAAM,WAAW,IAAI,CAAC,CAAC;AAC3E,qBAAe,IAAI;AACnB,eAAS,CAAC;AACV,mBAAa,CAAC;AAGd,kBAAY,KAAK,YAAY,CAAC,UAAwB;AACpD,cAAM,EAAE,SAAS,IAAI,MAAM;AAE3B,YAAI,CAAC,YAAY,SAAS,WAAW,GAAG;AACtC,kBAAQ,KAAK,2EAA2E;AACxF;AAAA,QACF;AAGA,iBAAS,KAAK,GAAG,KAAK,SAAS,QAAQ,MAAM;AAC3C,cAAI,CAAC,kBAAkB,QAAQ,EAAE,GAAG;AAClC,oBAAQ;AAAA,cACN,0CAA0C,EAAE,2BAA2B,kBAAkB,QAAQ,MAAM;AAAA,YACzG;AACA,8BAAkB,QAAQ,EAAE,IAAI,CAAC;AAAA,UACnC;AACA,4BAAkB,QAAQ,EAAE,EAAE,KAAK,SAAS,EAAE,CAAC;AAAA,QACjD;AAEA,cAAM,yBAAyB,gBAAgB;AAC/C,wBAAgB,WAAW,SAAS,CAAC,EAAE;AACvC,iBAAS,CAAC,cAAc;AAEtB,gBAAM,UAAsC,CAAC;AAC7C,mBAAS,KAAK,GAAG,KAAK,SAAS,QAAQ,MAAM;AAC3C,kBAAM,OAAO,UAAU,EAAE,KAAK,WAAW,IAAI;AAC7C,oBAAQ;AAAA,kBACN,yBAAY,MAAM,SAAS,EAAE,GAAG,iBAAiB,wBAAwB,IAAI;AAAA,YAC/E;AAAA,UACF;AACA,iBAAO;AAAA,QACT,CAAC;AAAA,MAIH;AAGA,aAAO,QAAQ,WAAW;AAE1B,kBAAY,KAAK,YAAY,EAAE,SAAS,SAAS,cAAc,mBAAmB,CAAC;AAGnF,YAAM,aAAa,OAAO,eAAe,EAAE,CAAC,KAAK;AACjD,oBAAc,UAAU;AACxB,UAAI,YAAY;AACd,cAAM,UAAU,MAAM;AACpB,kBAAQ,KAAK,kEAAkE;AAE/E,2BAAiB,UAAU;AAC3B,mBAAS,IAAI,MAAM,0CAA0C,CAAC;AAAA,QAChE;AACA,wBAAgB,UAAU;AAC1B,mBAAW,iBAAiB,SAAS,OAAO;AAAA,MAC9C;AAEA,qBAAe,UAAU;AACzB,kBAAY,UAAU;AACtB,qBAAe,IAAI;AACnB,kBAAY,KAAK;AACjB,mBAAa,UAAU,YAAY,IAAI;AACvC,wBAAkB;AAAA,IACpB,SAAS,KAAK;AACZ,cAAQ,KAAK,kDAAkD,OAAO,GAAG,CAAC;AAC1E,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,2BAA2B,CAAC;AAAA,IAC9E;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,iBAAiB,MAAM,aAAa,iBAAiB,CAAC;AAGhF,QAAM,oBAAgB,0BAAY,YAAyC;AACzE,QAAI,CAAC,eAAe,SAAS;AAC3B,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,UAAI,cAAc,WAAW,gBAAgB,SAAS;AACpD,sBAAc,QAAQ,oBAAoB,SAAS,gBAAgB,OAAO;AAC1E,sBAAc,UAAU;AACxB,wBAAgB,UAAU;AAAA,MAC5B;AAGA,UAAI,eAAe,SAAS;AAC1B,uBAAe,QAAQ,KAAK,YAAY,EAAE,SAAS,OAAO,CAAC;AAG3D,YAAI,qBAAqB,SAAS;AAChC,cAAI;AACF,iCAAqB,QAAQ,WAAW,eAAe,OAAO;AAAA,UAChE,SAAS,KAAK;AACZ,oBAAQ,KAAK,sDAAsD,OAAO,GAAG,CAAC;AAAA,UAChF;AAAA,QACF;AACA,uBAAe,QAAQ,WAAW;AAAA,MACpC;AAGA,UAAI,kBAAkB,YAAY,MAAM;AACtC,6BAAqB,kBAAkB,OAAO;AAC9C,0BAAkB,UAAU;AAAA,MAC9B;AAGA,YAAM,cAAU,iCAAiB;AACjC,YAAM,aAAa,QAAQ;AAC3B,YAAM,cAAc,kBAAkB,QAAQ,UAAU;AACxD,YAAM,cAAc,kBAAkB,QAAQ,IAAI,CAAC,eAAW,kCAAqB,MAAM,CAAC;AAC1F,YAAM,eAAe,YAAY,CAAC,GAAG,UAAU;AAI/C,UAAI,iBAAiB,GAAG;AACtB,gBAAQ,KAAK,iFAA4E;AACzF,uBAAe,UAAU;AACzB,oBAAY,UAAU;AACtB,uBAAe,KAAK;AACpB,oBAAY,KAAK;AACjB,iBAAS,CAAC;AACV,eAAO;AAAA,MACT;AAEA,YAAM,aAAS,+BAAkB,YAAY,aAAa,WAAW,YAAY,WAAW;AAE5F,qBAAe,MAAM;AACrB,kBAAY,OAAO,QAAQ;AAC3B,qBAAe,UAAU;AACzB,kBAAY,UAAU;AACtB,qBAAe,KAAK;AACpB,kBAAY,KAAK;AACjB,eAAS,CAAC;AAGV,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,cAAQ,KAAK,iDAAiD,OAAO,GAAG,CAAC;AACzE,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,0BAA0B,CAAC;AAC3E,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AACjB,mBAAiB,UAAU;AAG3B,QAAM,qBAAiB,0BAAY,MAAM;AACvC,QAAI,eAAe,CAAC,UAAU;AAC5B,UAAI,kBAAkB,YAAY,MAAM;AACtC,6BAAqB,kBAAkB,OAAO;AAC9C,0BAAkB,UAAU;AAAA,MAC9B;AACA,kBAAY,UAAU;AACtB,kBAAY,IAAI;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,aAAa,QAAQ,CAAC;AAG1B,QAAM,sBAAkB,0BAAY,MAAM;AACxC,QAAI,eAAe,UAAU;AAC3B,kBAAY,UAAU;AACtB,kBAAY,KAAK;AACjB,mBAAa,UAAU,YAAY,IAAI,IAAI,WAAW;AACtD,wBAAkB;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,aAAa,UAAU,UAAU,iBAAiB,CAAC;AAKvD,8BAAU,MAAM;AACd,WAAO,MAAM;AAEX,UAAI,cAAc,WAAW,gBAAgB,SAAS;AACpD,sBAAc,QAAQ,oBAAoB,SAAS,gBAAgB,OAAO;AAAA,MAC5E;AACA,UAAI,eAAe,SAAS;AAC1B,uBAAe,QAAQ,KAAK,YAAY,EAAE,SAAS,OAAO,CAAC;AAG3D,YAAI,qBAAqB,SAAS;AAChC,cAAI;AACF,iCAAqB,QAAQ,WAAW,eAAe,OAAO;AAAA,UAChE,SAAS,KAAK;AACZ,oBAAQ,KAAK,yDAAyD,OAAO,GAAG,CAAC;AAAA,UACnF;AAAA,QACF;AACA,YAAI;AACF,yBAAe,QAAQ,WAAW;AAAA,QACpC,SAAS,KAAK;AACZ,kBAAQ,KAAK,0DAA0D,OAAO,GAAG,CAAC;AAAA,QACpF;AAAA,MACF;AACA,UAAI,kBAAkB,YAAY,MAAM;AACtC,6BAAqB,kBAAkB,OAAO;AAAA,MAChD;AAAA,IAEF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC/VA,IAAAC,gBAAiD;AAG1C,SAAS,sBAAiD;AAC/D,QAAM,CAAC,QAAQ,SAAS,QAAI,wBAA6B,IAAI;AAC7D,QAAM,CAAC,SAAS,UAAU,QAAI,wBAA6B,CAAC,CAAC;AAC7D,QAAM,CAAC,eAAe,gBAAgB,QAAI,wBAAS,KAAK;AACxD,QAAM,CAAC,WAAW,YAAY,QAAI,wBAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAuB,IAAI;AAGrD,QAAM,uBAAmB,2BAAY,YAAY;AAC/C,QAAI;AACF,YAAM,aAAa,MAAM,UAAU,aAAa,iBAAiB;AACjE,YAAM,cAAc,WACjB,OAAO,CAAC,WAAW,OAAO,SAAS,YAAY,EAC/C,IAAI,CAAC,YAAY;AAAA,QAChB,UAAU,OAAO;AAAA,QACjB,OAAO,OAAO,SAAS,cAAc,OAAO,SAAS,MAAM,GAAG,CAAC,CAAC;AAAA,QAChE,SAAS,OAAO;AAAA,MAClB,EAAE;AAEJ,iBAAW,WAAW;AAAA,IACxB,SAAS,KAAK;AACZ,cAAQ,MAAM,gCAAgC,GAAG;AACjD,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,6BAA6B,CAAC;AAAA,IAChF;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,oBAAgB;AAAA,IACpB,OAAO,UAAmB,qBAA6C;AACrE,mBAAa,IAAI;AACjB,eAAS,IAAI;AAEb,UAAI;AAEF,YAAI,QAAQ;AACV,iBAAO,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAAA,QACpD;AAGA,cAAM,QAAsD;AAAA;AAAA,UAE1D,kBAAkB;AAAA,UAClB,kBAAkB;AAAA,UAClB,iBAAiB;AAAA,UACjB,SAAS;AAAA;AAAA;AAAA,UAET,GAAG;AAAA;AAAA,UAEH,GAAI,YAAY,EAAE,UAAU,EAAE,OAAO,SAAS,EAAE;AAAA,QAClD;AAEA,cAAM,cAAsC;AAAA,UAC1C;AAAA,UACA,OAAO;AAAA,QACT;AAEA,cAAM,YAAY,MAAM,UAAU,aAAa,aAAa,WAAW;AACvE,kBAAU,SAAS;AACnB,yBAAiB,IAAI;AAGrB,cAAM,iBAAiB;AAAA,MACzB,SAAS,KAAK;AACZ,gBAAQ,MAAM,gCAAgC,GAAG;AACjD,iBAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,6BAA6B,CAAC;AAC9E,yBAAiB,KAAK;AAAA,MACxB,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,gBAAgB;AAAA,EAC3B;AAGA,QAAM,iBAAa,2BAAY,MAAM;AACnC,QAAI,QAAQ;AACV,aAAO,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAClD,gBAAU,IAAI;AACd,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAGX,+BAAU,MAAM;AAEd,qBAAiB;AAGjB,cAAU,aAAa,iBAAiB,gBAAgB,gBAAgB;AAGxE,WAAO,MAAM;AACX,gBAAU,aAAa,oBAAoB,gBAAgB,gBAAgB;AAC3E,UAAI,QAAQ;AACV,eAAO,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF,GAAG,CAAC,kBAAkB,MAAM,CAAC;AAE7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC5GA,IAAAC,gBAAyD;AACzD,IAAAC,kBAAiC;AACjC,IAAAC,eAAiC;AACjC,IAAAC,mBAAyD;AAGzD,IAAM,aAAa;AA2EZ,SAAS,mBACd,QACA,UAAqC,CAAC,GACZ;AAC1B,QAAM,EAAE,aAAa,IAAI,eAAe,EAAE,IAAI;AAE9C,QAAM,CAAC,QAAQ,SAAS,QAAI,wBAAmB,MAAM,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AACpF,QAAM,CAAC,YAAY,aAAa,QAAI,wBAAmB,MAAM,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAC5F,QAAM,CAAC,WAAW,YAAY,QAAI,wBAAmB,MAAM,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAC1F,QAAM,CAAC,YAAY,aAAa,QAAI,wBAAuB,IAAI;AAE/D,QAAM,qBAAiB,sBAAgC,IAAI;AAC3D,QAAM,gBAAY,sBAA0C,IAAI;AAChE,QAAM,sBAAkB,sBAAiB,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAExE,QAAM,gBAAY;AAAA,IAChB,MAAM,cAAc,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAAA,IACnD,CAAC,YAAY;AAAA,EACf;AAEA,+BAAU,MAAM;AACd,QAAI,CAAC,QAAQ;AACX,gBAAU,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AACzC,oBAAc,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAC7C,mBAAa,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC,CAAC;AAC5C,sBAAgB,UAAU,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC;AACxD;AAAA,IACF;AAEA,QAAI,YAAY;AAEhB,UAAM,kBAAkB,YAAY;AAClC,UAAI,CAAC,UAAW;AAEhB,YAAM,cAAU,kCAAiB;AACjC,UAAI,QAAQ,UAAU,aAAa;AACjC,cAAM,QAAQ,OAAO;AAAA,MACvB;AACA,UAAI,CAAC,UAAW;AAGhB,YAAM,gBAAgB,OAAO,eAAe,EAAE,CAAC,GAAG,YAAY;AAC9D,YAAM,iBAAiB,eAAe,gBAAgB;AAKtD,YAAM,SAAS,QAAQ;AACvB,gBAAM,wCAAsB,CAAC,QAAQ,OAAO,aAAa,UAAU,GAAG,CAAC;AACvE,UAAI,CAAC,UAAW;AAIhB,YAAM,cAAc,QAAQ,uBAAuB,mBAAmB;AAAA,QACpE,cAAc;AAAA,QACd,kBAAkB;AAAA,QAClB,kBAAkB;AAAA,UAChB,kBAAkB;AAAA,UAClB;AAAA,QACF;AAAA,MACF,CAAC;AACD,qBAAe,UAAU;AAEzB,kBAAY,mBAAmB,CAAC,UAAU;AACxC,gBAAQ,KAAK,0DAA0D,OAAO,KAAK,CAAC;AAAA,MACtF;AAIA,YAAM,SAAS,QAAQ,wBAAwB,MAAM;AACrD,gBAAU,UAAU;AACpB,aAAO,QAAQ,WAAW;AAE1B,sBAAgB,UAAU,IAAI,MAAM,cAAc,EAAE,KAAK,CAAC;AAG1D,kBAAY,KAAK,YAAY,CAAC,UAAwB;AACpD,YAAI,CAAC,UAAW;AAEhB,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM;AAC5B,cAAM,WAAW,gBAAgB;AAEjC,cAAM,aAAuB,CAAC;AAC9B,cAAM,YAAsB,CAAC;AAE7B,iBAAS,KAAK,GAAG,KAAK,KAAK,QAAQ,MAAM;AACvC,mBAAS,EAAE,IAAI,KAAK,IAAI,KAAK,EAAE,IAAI,SAAS,EAAE,KAAK,KAAK,UAAU;AAClE,qBAAW,SAAK,+BAAiB,SAAS,EAAE,CAAC,CAAC;AAC9C,oBAAU,SAAK,+BAAiB,IAAI,EAAE,CAAC,CAAC;AAAA,QAC1C;AAGA,cAAM,gBACJ,KAAK,SAAS,eAAe,IAAI,MAAM,YAAY,EAAE,KAAK,WAAW,CAAC,CAAC,IAAI;AAC7E,cAAM,cACJ,KAAK,SAAS,eAAe,IAAI,MAAM,YAAY,EAAE,KAAK,UAAU,CAAC,CAAC,IAAI;AAE5E,kBAAU,aAAa;AACvB,qBAAa,WAAW;AACxB,sBAAc,CAAC,SAAS,cAAc,IAAI,CAAC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;AAAA,MACpF;AAAA,IACF;AAEA,oBAAgB,EAAE,MAAM,CAAC,QAAQ;AAC/B,cAAQ,KAAK,8DAA8D,OAAO,GAAG,CAAC;AACtF,UAAI,WAAW;AACb,sBAAc,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,MACnE;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,kBAAY;AAEZ,UAAI,UAAU,SAAS;AACrB,YAAI;AACF,oBAAU,QAAQ,WAAW;AAAA,QAC/B,SAAS,KAAK;AACZ,kBAAQ,KAAK,6DAA6D,OAAO,GAAG,CAAC;AAAA,QACvF;AACA,kBAAU,UAAU;AAAA,MACtB;AAEA,UAAI,eAAe,SAAS;AAC1B,YAAI;AACF,yBAAe,QAAQ,WAAW;AAClC,yBAAe,QAAQ,KAAK,MAAM;AAAA,QACpC,SAAS,KAAK;AACZ,kBAAQ,KAAK,4DAA4D,OAAO,GAAG,CAAC;AAAA,QACtF;AACA,uBAAe,UAAU;AAAA,MAC3B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,YAAY,YAAY,CAAC;AAGrC,QAAM,QAAQ,iBAAiB,IAAK,OAAO,CAAC,KAAK,IAAK,KAAK,IAAI,GAAG,MAAM;AACxE,QAAM,YAAY,iBAAiB,IAAK,WAAW,CAAC,KAAK,IAAK,KAAK,IAAI,GAAG,UAAU;AAEpF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT;AACF;;;ACtOA,IAAAC,gBAAyD;AAMzD,IAAAC,kBAIO;AA6DA,SAAS,uBACd,QACA,WACA,iBACA,UAAsC,CAAC,GACT;AAC9B,QAAM,EAAE,cAAc,GAAG,kBAAkB,GAAG,iBAAiB,IAAI;AAGnE,QAAM,CAAC,cAAc,eAAe,QAAI,wBAAS,KAAK;AACtD,QAAM,CAAC,gBAAgB,iBAAiB,QAAI,wBAAwB,IAAI;AACxE,QAAM,CAAC,WAAW,YAAY,QAAI,wBAAuB,IAAI;AAG7D,QAAM,4BAAwB,sBAAO,CAAC;AAKtC,QAAM,yBAAqB,sBAAO,eAAe;AACjD,qBAAmB,UAAU;AAC7B,QAAM,qBAAiB,sBAAO,WAAW;AACzC,iBAAe,UAAU;AAGzB,QAAM,EAAE,QAAQ,SAAS,eAAe,eAAe,OAAO,SAAS,IAAI,oBAAoB;AAG/F,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT,IAAI,mBAAmB,QAAQ;AAAA,IAC7B,cAAc,iBAAiB;AAAA,EACjC,CAAC;AAGD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT,IAAI,aAAa,QAAQ,gBAAgB;AAKzC,QAAM,qBAAiB,2BAAY,YAAY;AAC7C,QAAI,CAAC,mBAAmB,SAAS;AAC/B;AAAA,QACE,IAAI,MAAM,4EAA4E;AAAA,MACxF;AACA;AAAA,IACF;AAEA,QAAI;AACF,mBAAa,IAAI;AAEjB,UAAI,CAAC,cAAc;AACjB,kBAAM,0CAAyB;AAC/B,wBAAgB,IAAI;AAAA,MACtB;AAKA,4BAAsB,UAAU,eAAe;AAE/C,YAAM,SAAS;AAAA,IACjB,SAAS,KAAK;AACZ,mBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAClE;AAAA,EACF,GAAG,CAAC,cAAc,QAAQ,CAAC;AAG3B,QAAM,oBAAgB,2BAAY,YAAY;AAC5C,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,QAAQ;AAAA,IACzB,SAAS,KAAK;AACZ,mBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAChE;AAAA,IACF;AAGA,UAAM,UAAU,mBAAmB;AACnC,QAAI,UAAU,SAAS;AACrB,YAAM,qBAAqB,OAAO,UAAU,CAAC,MAAM,EAAE,OAAO,OAAO;AACnE,UAAI,uBAAuB,IAAI;AAC7B,cAAM,MAAM,IAAI;AAAA,UACd,kCAAkC,OAAO;AAAA,QAC3C;AACA,gBAAQ,KAAK,uBAAuB,IAAI,OAAO,EAAE;AACjD,qBAAa,GAAG;AAChB;AAAA,MACF;AAEA,YAAM,gBAAgB,OAAO,kBAAkB;AAG/C,YAAM,yBAAyB,KAAK,MAAM,sBAAsB,UAAU,OAAO,UAAU;AAE3F,UAAI,oBAAoB;AACxB,UAAI,cAAc,MAAM,SAAS,GAAG;AAClC,cAAM,aAAa,cAAc,MAAM;AAAA,UACrC,CAAC,SAAS,KAAK,cAAc,KAAK;AAAA,QACpC;AACA,4BAAoB,KAAK,IAAI,GAAG,UAAU;AAAA,MAC5C;AAEA,YAAM,cAAc,KAAK,IAAI,wBAAwB,iBAAiB;AAQtE,YAAM,mBAAe,uCAAsB;AAC3C,YAAM,gBAAgB,aAAa,iBAAiB;AACpD,YAAM,kBAAc,kCAAiB;AACrC,YAAM,YAAY,YAAY,aAAa;AAC3C,YAAM,eAAe,gBAAgB;AACrC,YAAM,uBAAuB,KAAK,MAAM,eAAe,OAAO,UAAU;AAGxE,YAAM,oBAAoB,KAAK,IAAI,GAAG,OAAO,SAAS,oBAAoB;AAC1E,UAAI,sBAAsB,GAAG;AAC3B,gBAAQ;AAAA,UACN;AAAA,QACF;AACA,qBAAa,IAAI,MAAM,4DAA4D,CAAC;AACpF;AAAA,MACF;AAGA,YAAM,UAAqB;AAAA,QACzB,IAAI,QAAQ,KAAK,IAAI,CAAC;AAAA,QACtB,aAAa;AAAA,QACb;AAAA,QACA,iBAAiB;AAAA,QACjB,eAAe;AAAA,QACf,YAAY,OAAO;AAAA,QACnB,uBAAuB,OAAO;AAAA,QAC9B,MAAM;AAAA,QACN,MAAM,cAAa,oBAAI,KAAK,GAAE,mBAAmB,CAAC;AAAA,MACpD;AAGA,YAAM,YAAY,OAAO,IAAI,CAAC,OAAO,UAAU;AAC7C,YAAI,UAAU,oBAAoB;AAChC,iBAAO;AAAA,YACL,GAAG;AAAA,YACH,OAAO,CAAC,GAAG,MAAM,OAAO,OAAO;AAAA,UACjC;AAAA,QACF;AACA,eAAO;AAAA,MACT,CAAC;AAED,gBAAU,SAAS;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,QAAQ,WAAW,OAAO,CAAC;AAG/B,+BAAU,MAAM;AACd,QAAI,CAAC,iBAAiB,QAAQ,WAAW,EAAG;AAE5C,QAAI,mBAAmB,MAAM;AAE3B,wBAAkB,QAAQ,CAAC,EAAE,QAAQ;AAAA,IACvC,WAAW,CAAC,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,cAAc,GAAG;AAE9D,YAAM,aAAa,QAAQ,CAAC,EAAE;AAC9B,wBAAkB,UAAU;AAC5B,gBAAU;AACV,oBAAc,YAAY,gBAAgB;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,eAAe,SAAS,gBAAgB,WAAW,eAAe,gBAAgB,CAAC;AAGvF,QAAM,uBAAmB,2BAAY,YAAY;AAC/C,QAAI;AACF,mBAAa,IAAI;AACjB,YAAM,cAAc,QAAW,gBAAgB;AAC/C,gBAAM,0CAAyB;AAC/B,sBAAgB,IAAI;AAAA,IACtB,SAAS,KAAK;AACZ,mBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAClE;AAAA,EACF,GAAG,CAAC,eAAe,gBAAgB,CAAC;AAGpC,QAAM,mBAAe;AAAA,IACnB,OAAO,aAAqB;AAC1B,UAAI;AACF,qBAAa,IAAI;AACjB,0BAAkB,QAAQ;AAC1B,kBAAU;AACV,cAAM,cAAc,UAAU,gBAAgB;AAC9C,kBAAM,0CAAyB;AAC/B,wBAAgB,IAAI;AAAA,MACtB,SAAS,KAAK;AACZ,qBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,MAClE;AAAA,IACF;AAAA,IACA,CAAC,eAAe,kBAAkB,SAAS;AAAA,EAC7C;AAEA,SAAO;AAAA;AAAA,IAEL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,aAAa,YAAY,cAAc;AAAA;AAAA,IAG9C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA,gBAAgB;AAAA,EAClB;AACF;","names":["error","import_react","import_react","import_playout","import_core","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 { addRecordingWorkletModule } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n // Resolved when the worklet posts the final done message after a stop command.\n const stopAckResolveRef = useRef<(() => void) | null>(null);\n // True from the start of stopRecording until it resolves. Distinct from\n // isPausedRef (user explicitly paused) — both freeze the duration tick,\n // but conflating them risks pauseRecording becoming a no-op while we're\n // stopping, or vice versa.\n const isStoppingRef = useRef<boolean>(false);\n\n // Shared duration update loop — starts a rAF loop that updates duration\n // from performance.now(). Used by both startRecording and resumeRecording.\n const startDurationLoop = useCallback(() => {\n const tick = () => {\n if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(tick);\n }\n };\n tick();\n }, []);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getGlobalContext();\n // Use addRecordingWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addRecordingWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels, done } = event.data as {\n channels: Float32Array[];\n done?: boolean;\n };\n\n // Empty messages can arrive from the final stop ack when nothing was buffered.\n // Handle channels first if present, then resolve the stop barrier.\n if (channels && channels.length > 0 && channels[0] && channels[0].length > 0) {\n for (let ch = 0; ch < channels.length; ch++) {\n if (!recordedChunksRef.current[ch]) {\n console.warn(\n `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`\n );\n recordedChunksRef.current[ch] = [];\n }\n recordedChunksRef.current[ch].push(channels[ch]);\n }\n // Capture sample offset before incrementing — used by peak alignment\n const samplesProcessedBefore = totalSamplesRef.current;\n totalSamplesRef.current += channels[0].length;\n\n // Skip live-preview peak updates while stop is in flight. The\n // queue may have many flushes pending; updating peaks for each\n // would let the live-preview waveform grow visibly past where\n // the user stopped, only to be replaced moments later by the\n // final clip (which is shorter due to latency compensation).\n // Chunks are already pushed above so no audio is lost.\n if (stopAckResolveRef.current === null) {\n setPeaks((prevPeaks) => {\n // Ensure we have an entry per channel\n const updated: (Int8Array | Int16Array)[] = [];\n for (let ch = 0; ch < channels.length; ch++) {\n const prev = prevPeaks[ch] ?? emptyPeaks(bits);\n updated.push(\n appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)\n );\n }\n return updated;\n });\n }\n }\n\n if (done) {\n stopAckResolveRef.current?.();\n stopAckResolveRef.current = null;\n }\n\n // Note: VU meter levels come from useMicrophoneLevel (meter-processor worklet)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Connect and start — after state reset and handler setup\n source.connect(workletNode);\n // Do NOT send sampleRate — worklet uses its global sampleRate (always correct)\n workletNode.port.postMessage({ command: 'start', channelCount: streamChannelCount });\n\n // Listen on MediaStreamTrack for mic unplug (MediaStream has no 'ended' event)\n const audioTrack = stream.getAudioTracks()[0] ?? null;\n audioTrackRef.current = audioTrack;\n if (audioTrack) {\n const onEnded = () => {\n console.warn('[waveform-playlist] Audio track ended (mic unplugged or revoked)');\n // Stop recording to clean up worklet, rAF loop, and UI state\n stopRecordingRef.current?.();\n setError(new Error('Microphone disconnected during recording'));\n };\n onTrackEndedRef.current = onEnded;\n audioTrack.addEventListener('ended', onEnded);\n }\n\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n startDurationLoop();\n } catch (err) {\n console.warn('[waveform-playlist] Failed to start recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecordingRef.current) {\n return null;\n }\n\n try {\n // Freeze the duration tick immediately so the live-preview width\n // (durationSamples = duration * sampleRate) doesn't keep growing\n // during the worklet stop handshake / queue drain. isStoppingRef\n // stops the rAF tick from rescheduling; cancelling animationFrameRef\n // kills any frame already queued.\n isStoppingRef.current = true;\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n audioTrackRef.current = null;\n onTrackEndedRef.current = null;\n }\n\n // Stop the worklet and wait for its final flush message before reading chunks.\n // Without the await, the partial buffer at the end of recording arrives after\n // the AudioBuffer is built and is silently dropped.\n // Snapshot the worklet node once — the drain loop yields the event loop\n // for up to 250ms, during which an unmount-cleanup could null the ref.\n const node = workletNodeRef.current;\n if (node) {\n const stopAck = new Promise<void>((resolve) => {\n stopAckResolveRef.current = resolve;\n });\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n // Safety timeout (1s) — purely a circuit breaker for a truly failed\n // worklet (crashed, context closed). The drain loop below catches\n // any straggler flush messages that were queued before the timeout\n // fired, so chunks are complete even when this fires.\n const timeout = new Promise<void>((resolve) => {\n timeoutId = setTimeout(resolve, 1000);\n });\n\n node.port.postMessage({ command: 'stop' });\n await Promise.race([stopAck, timeout]);\n clearTimeout(timeoutId);\n stopAckResolveRef.current = null;\n\n // Drain the event-loop queue. During recording the worklet posts a\n // flush every ~16ms; if main was slower than that, messages back up.\n // Yield repeatedly until totalSamplesRef stabilizes for several\n // consecutive ticks — at that point the queue is empty.\n let lastSamples = -1;\n let stable = 0;\n for (let i = 0; i < 50; i++) {\n if (totalSamplesRef.current === lastSamples) {\n if (++stable >= 3) break;\n } else {\n stable = 0;\n lastSamples = totalSamplesRef.current;\n }\n await new Promise<void>((r) => setTimeout(r, 5));\n }\n\n // Null the handler so any late delivery from this worklet doesn't\n // contaminate a subsequent recording session's chunks.\n node.port.onmessage = null;\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(node);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during stop:', String(err));\n }\n }\n node.disconnect();\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to stop recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n } finally {\n // Always clear recording flags, even on failure — otherwise the UI gets\n // stuck in \"recording\" state if AudioBuffer creation throws.\n isRecordingRef.current = false;\n isPausedRef.current = false;\n isStoppingRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n workletNodeRef.current?.port.postMessage({ command: 'pause' });\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n workletNodeRef.current?.port.postMessage({ command: 'resume' });\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n startDurationLoop();\n }\n }, [isRecording, isPaused, duration, startDurationLoop]);\n\n // Cleanup on unmount — read refs in cleanup, not effect body.\n // Pattern #16 (copy refs in effect body) doesn't apply to [] deps — refs are\n // null at mount and only set later during startRecording.\n useEffect(() => {\n return () => {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n }\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during cleanup:', String(err));\n }\n }\n try {\n workletNodeRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Worklet disconnect during cleanup:', String(err));\n }\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(\n async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(err instanceof Error ? err : new Error('Failed to access microphone'));\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n },\n [stream, enumerateDevices]\n );\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state, enumerate devices, and listen for hot-plug changes\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Re-enumerate when devices are plugged in or removed\n navigator.mediaDevices.addEventListener('devicechange', enumerateDevices);\n\n // Cleanup on unmount\n return () => {\n navigator.mediaDevices.removeEventListener('devicechange', enumerateDevices);\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, [enumerateDevices, stream]);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses an AudioWorklet-based meter processor for sample-accurate\n * peak and RMS metering without requestAnimationFrame overhead.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { addMeterWorkletModule, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Use addMeterWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addMeterWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip, audibleLatencySamples } from '@waveform-playlist/core';\nimport {\n resumeGlobalAudioContext,\n getGlobalAudioContext,\n getGlobalContext,\n} from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n /** Per-channel peak levels (0-1). Array length matches channelCount. */\n levels: number[];\n /** Per-channel held peak levels (0-1). Array length matches channelCount. */\n peakLevels: number[];\n /** Per-channel RMS levels (0-1). Array length matches channelCount. */\n rmsLevels: number[];\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: (Int8Array | Int16Array)[];\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Capture timeline position when recording starts (not at stop time)\n const recordingStartTimeRef = useRef(0);\n\n // Keep selectedTrackId and currentTime in refs for use in callbacks.\n // Avoids stale closures and prevents 60fps useCallback recreation\n // (currentTime updates at animation-frame rate during playback).\n const selectedTrackIdRef = useRef(selectedTrackId);\n selectedTrackIdRef.current = selectedTrackId;\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n\n // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const {\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n resetPeak,\n error: meterError,\n } = useMicrophoneLevel(stream, {\n channelCount: recordingOptions.channelCount,\n });\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _recordedAudioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n // Reads selectedTrackId from ref to avoid stale closures when\n // auto-create track + start recording happen in the same render cycle\n const startRecording = useCallback(async () => {\n if (!selectedTrackIdRef.current) {\n setHookError(\n new Error('Cannot start recording: no track selected. Select or create a track first.')\n );\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n // Capture timeline position NOW — before recording starts.\n // Using currentTime at stop time would be wrong during overdub\n // (playback advances currentTime while recording).\n recordingStartTimeRef.current = currentTimeRef.current;\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n const trackId = selectedTrackIdRef.current;\n if (buffer && trackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === trackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${trackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.warn(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Use the captured start time (not live currentTime which advances during overdub)\n const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);\n\n // Latency compensation:\n // Two sources of delay between recording start and audible playback:\n // 1. Tone.js lookAhead (~100ms) — Transport schedules audio ahead of real time\n // 2. Output latency — hardware DAC delay before audio reaches speakers\n // The user hears playback delayed by both, so they perform late relative\n // to the timeline. Skip that duration at the start of the recorded audio.\n // Shared formula with the live preview in PlaylistVisualization — keep both\n // paths using `audibleLatencySamples` so trim widths match.\n const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const latencyOffsetSamples = audibleLatencySamples(\n outputLatency,\n lookAhead,\n buffer.sampleRate\n );\n\n // Guard: very short recordings (< latency compensation) would produce negative duration\n const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);\n if (effectiveDuration === 0) {\n console.warn(\n '[waveform-playlist] Recording too short for latency compensation — discarding'\n );\n setHookError(new Error('Recording was too short to save. Try recording for longer.'));\n return;\n }\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: effectiveDuration,\n offsetSamples: latencyOffsetSamples,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [tracks, setTracks, stopRec]);\n\n // Auto-select first device when available, or fallback if selected device was unplugged\n useEffect(() => {\n if (!hasPermission || devices.length === 0) return;\n\n if (selectedDevice === null) {\n // First-time selection\n setSelectedDevice(devices[0].deviceId);\n } else if (!devices.some((d) => d.deviceId === selectedDevice)) {\n // Selected device was removed — fall back to first available\n const fallbackId = devices[0].deviceId;\n setSelectedDevice(fallbackId);\n resetPeak();\n requestAccess(fallbackId, audioConstraints);\n }\n }, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(\n async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n resetPeak();\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [requestAccess, audioConstraints, resetPeak]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n error: hookError || micError || meterError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n"],"mappings":"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","names":["error","import_react","import_react","import_playout","import_core","import_worklets","import_react","import_core","import_playout"]}
|
package/dist/index.mjs
CHANGED
|
@@ -28,9 +28,11 @@ function useRecording(stream, options = {}) {
|
|
|
28
28
|
const audioTrackRef = useRef(null);
|
|
29
29
|
const onTrackEndedRef = useRef(null);
|
|
30
30
|
const stopRecordingRef = useRef(null);
|
|
31
|
+
const stopAckResolveRef = useRef(null);
|
|
32
|
+
const isStoppingRef = useRef(false);
|
|
31
33
|
const startDurationLoop = useCallback(() => {
|
|
32
34
|
const tick = () => {
|
|
33
|
-
if (isRecordingRef.current && !isPausedRef.current) {
|
|
35
|
+
if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {
|
|
34
36
|
const elapsed = (performance.now() - startTimeRef.current) / 1e3;
|
|
35
37
|
setDuration(elapsed);
|
|
36
38
|
animationFrameRef.current = requestAnimationFrame(tick);
|
|
@@ -91,32 +93,36 @@ function useRecording(stream, options = {}) {
|
|
|
91
93
|
setLevel(0);
|
|
92
94
|
setPeakLevel(0);
|
|
93
95
|
workletNode.port.onmessage = (event) => {
|
|
94
|
-
const { channels } = event.data;
|
|
95
|
-
if (
|
|
96
|
-
console.warn("[waveform-playlist] Recording worklet sent empty or missing channels data");
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
for (let ch = 0; ch < channels.length; ch++) {
|
|
100
|
-
if (!recordedChunksRef.current[ch]) {
|
|
101
|
-
console.warn(
|
|
102
|
-
`[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`
|
|
103
|
-
);
|
|
104
|
-
recordedChunksRef.current[ch] = [];
|
|
105
|
-
}
|
|
106
|
-
recordedChunksRef.current[ch].push(channels[ch]);
|
|
107
|
-
}
|
|
108
|
-
const samplesProcessedBefore = totalSamplesRef.current;
|
|
109
|
-
totalSamplesRef.current += channels[0].length;
|
|
110
|
-
setPeaks((prevPeaks) => {
|
|
111
|
-
const updated = [];
|
|
96
|
+
const { channels, done } = event.data;
|
|
97
|
+
if (channels && channels.length > 0 && channels[0] && channels[0].length > 0) {
|
|
112
98
|
for (let ch = 0; ch < channels.length; ch++) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
if (!recordedChunksRef.current[ch]) {
|
|
100
|
+
console.warn(
|
|
101
|
+
`[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`
|
|
102
|
+
);
|
|
103
|
+
recordedChunksRef.current[ch] = [];
|
|
104
|
+
}
|
|
105
|
+
recordedChunksRef.current[ch].push(channels[ch]);
|
|
117
106
|
}
|
|
118
|
-
|
|
119
|
-
|
|
107
|
+
const samplesProcessedBefore = totalSamplesRef.current;
|
|
108
|
+
totalSamplesRef.current += channels[0].length;
|
|
109
|
+
if (stopAckResolveRef.current === null) {
|
|
110
|
+
setPeaks((prevPeaks) => {
|
|
111
|
+
const updated = [];
|
|
112
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
113
|
+
const prev = prevPeaks[ch] ?? emptyPeaks(bits);
|
|
114
|
+
updated.push(
|
|
115
|
+
appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return updated;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (done) {
|
|
123
|
+
stopAckResolveRef.current?.();
|
|
124
|
+
stopAckResolveRef.current = null;
|
|
125
|
+
}
|
|
120
126
|
};
|
|
121
127
|
source.connect(workletNode);
|
|
122
128
|
workletNode.port.postMessage({ command: "start", channelCount: streamChannelCount });
|
|
@@ -147,25 +153,49 @@ function useRecording(stream, options = {}) {
|
|
|
147
153
|
return null;
|
|
148
154
|
}
|
|
149
155
|
try {
|
|
156
|
+
isStoppingRef.current = true;
|
|
157
|
+
if (animationFrameRef.current !== null) {
|
|
158
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
159
|
+
animationFrameRef.current = null;
|
|
160
|
+
}
|
|
150
161
|
if (audioTrackRef.current && onTrackEndedRef.current) {
|
|
151
162
|
audioTrackRef.current.removeEventListener("ended", onTrackEndedRef.current);
|
|
152
163
|
audioTrackRef.current = null;
|
|
153
164
|
onTrackEndedRef.current = null;
|
|
154
165
|
}
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
const node = workletNodeRef.current;
|
|
167
|
+
if (node) {
|
|
168
|
+
const stopAck = new Promise((resolve) => {
|
|
169
|
+
stopAckResolveRef.current = resolve;
|
|
170
|
+
});
|
|
171
|
+
let timeoutId;
|
|
172
|
+
const timeout = new Promise((resolve) => {
|
|
173
|
+
timeoutId = setTimeout(resolve, 1e3);
|
|
174
|
+
});
|
|
175
|
+
node.port.postMessage({ command: "stop" });
|
|
176
|
+
await Promise.race([stopAck, timeout]);
|
|
177
|
+
clearTimeout(timeoutId);
|
|
178
|
+
stopAckResolveRef.current = null;
|
|
179
|
+
let lastSamples = -1;
|
|
180
|
+
let stable = 0;
|
|
181
|
+
for (let i = 0; i < 50; i++) {
|
|
182
|
+
if (totalSamplesRef.current === lastSamples) {
|
|
183
|
+
if (++stable >= 3) break;
|
|
184
|
+
} else {
|
|
185
|
+
stable = 0;
|
|
186
|
+
lastSamples = totalSamplesRef.current;
|
|
187
|
+
}
|
|
188
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
189
|
+
}
|
|
190
|
+
node.port.onmessage = null;
|
|
157
191
|
if (mediaStreamSourceRef.current) {
|
|
158
192
|
try {
|
|
159
|
-
mediaStreamSourceRef.current.disconnect(
|
|
193
|
+
mediaStreamSourceRef.current.disconnect(node);
|
|
160
194
|
} catch (err) {
|
|
161
195
|
console.warn("[waveform-playlist] Source disconnect during stop:", String(err));
|
|
162
196
|
}
|
|
163
197
|
}
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
if (animationFrameRef.current !== null) {
|
|
167
|
-
cancelAnimationFrame(animationFrameRef.current);
|
|
168
|
-
animationFrameRef.current = null;
|
|
198
|
+
node.disconnect();
|
|
169
199
|
}
|
|
170
200
|
const context = getGlobalContext();
|
|
171
201
|
const rawContext = context.rawContext;
|
|
@@ -174,31 +204,29 @@ function useRecording(stream, options = {}) {
|
|
|
174
204
|
const totalSamples = channelData[0]?.length ?? 0;
|
|
175
205
|
if (totalSamples === 0) {
|
|
176
206
|
console.warn("[waveform-playlist] Recording stopped with 0 samples captured \u2014 discarding");
|
|
177
|
-
isRecordingRef.current = false;
|
|
178
|
-
isPausedRef.current = false;
|
|
179
|
-
setIsRecording(false);
|
|
180
|
-
setIsPaused(false);
|
|
181
|
-
setLevel(0);
|
|
182
207
|
return null;
|
|
183
208
|
}
|
|
184
209
|
const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);
|
|
185
210
|
setAudioBuffer(buffer);
|
|
186
211
|
setDuration(buffer.duration);
|
|
187
|
-
isRecordingRef.current = false;
|
|
188
|
-
isPausedRef.current = false;
|
|
189
|
-
setIsRecording(false);
|
|
190
|
-
setIsPaused(false);
|
|
191
|
-
setLevel(0);
|
|
192
212
|
return buffer;
|
|
193
213
|
} catch (err) {
|
|
194
214
|
console.warn("[waveform-playlist] Failed to stop recording:", String(err));
|
|
195
215
|
setError(err instanceof Error ? err : new Error("Failed to stop recording"));
|
|
196
216
|
return null;
|
|
217
|
+
} finally {
|
|
218
|
+
isRecordingRef.current = false;
|
|
219
|
+
isPausedRef.current = false;
|
|
220
|
+
isStoppingRef.current = false;
|
|
221
|
+
setIsRecording(false);
|
|
222
|
+
setIsPaused(false);
|
|
223
|
+
setLevel(0);
|
|
197
224
|
}
|
|
198
225
|
}, [channelCount]);
|
|
199
226
|
stopRecordingRef.current = stopRecording;
|
|
200
227
|
const pauseRecording = useCallback(() => {
|
|
201
228
|
if (isRecording && !isPaused) {
|
|
229
|
+
workletNodeRef.current?.port.postMessage({ command: "pause" });
|
|
202
230
|
if (animationFrameRef.current !== null) {
|
|
203
231
|
cancelAnimationFrame(animationFrameRef.current);
|
|
204
232
|
animationFrameRef.current = null;
|
|
@@ -209,6 +237,7 @@ function useRecording(stream, options = {}) {
|
|
|
209
237
|
}, [isRecording, isPaused]);
|
|
210
238
|
const resumeRecording = useCallback(() => {
|
|
211
239
|
if (isRecording && isPaused) {
|
|
240
|
+
workletNodeRef.current?.port.postMessage({ command: "resume" });
|
|
212
241
|
isPausedRef.current = false;
|
|
213
242
|
setIsPaused(false);
|
|
214
243
|
startTimeRef.current = performance.now() - duration * 1e3;
|
|
@@ -460,6 +489,7 @@ function useMicrophoneLevel(stream, options = {}) {
|
|
|
460
489
|
|
|
461
490
|
// src/hooks/useIntegratedRecording.ts
|
|
462
491
|
import { useState as useState4, useCallback as useCallback4, useEffect as useEffect4, useRef as useRef3 } from "react";
|
|
492
|
+
import { audibleLatencySamples } from "@waveform-playlist/core";
|
|
463
493
|
import {
|
|
464
494
|
resumeGlobalAudioContext,
|
|
465
495
|
getGlobalAudioContext,
|
|
@@ -551,8 +581,11 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
551
581
|
const outputLatency = audioContext.outputLatency ?? 0;
|
|
552
582
|
const toneContext = getGlobalContext3();
|
|
553
583
|
const lookAhead = toneContext.lookAhead ?? 0;
|
|
554
|
-
const
|
|
555
|
-
|
|
584
|
+
const latencyOffsetSamples = audibleLatencySamples(
|
|
585
|
+
outputLatency,
|
|
586
|
+
lookAhead,
|
|
587
|
+
buffer.sampleRate
|
|
588
|
+
);
|
|
556
589
|
const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);
|
|
557
590
|
if (effectiveDuration === 0) {
|
|
558
591
|
console.warn(
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer, appendPeaks } from '@waveform-playlist/core';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { addRecordingWorkletModule } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n\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 // Use addRecordingWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addRecordingWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels } = 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 { addMeterWorkletModule, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Use addMeterWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addMeterWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip } 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"]}
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useRecording.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts"],"sourcesContent":["/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer, appendPeaks } from '@waveform-playlist/core';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { addRecordingWorkletModule } from '@waveform-playlist/worklets';\n\nfunction emptyPeaks(bits: 8 | 16): Int8Array | Int16Array {\n return bits === 8 ? new Int8Array(0) : new Int16Array(0);\n}\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const { channelCount = 1, samplesPerPixel = 1024, bits = 16 } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n // Per-channel peaks for multi-channel live preview\n const [peaks, setPeaks] = useState<(Int8Array | Int16Array)[]>([emptyPeaks(bits)]);\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n // Per-instance flag to prevent loading worklet multiple times within the same hook instance.\n // Note: Multiple hook instances each have their own ref — see \"Multi-Instance Worklet\n // Registration Gap\" in recording/CLAUDE.md for the known limitation and planned fix.\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n // Per-channel sample accumulation: recordedChunksRef[channelIndex] = Float32Array[]\n const recordedChunksRef = useRef<Float32Array[][]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n const audioTrackRef = useRef<MediaStreamTrack | null>(null);\n const onTrackEndedRef = useRef<(() => void) | null>(null);\n const stopRecordingRef = useRef<(() => Promise<AudioBuffer | null>) | null>(null);\n // Resolved when the worklet posts the final done message after a stop command.\n const stopAckResolveRef = useRef<(() => void) | null>(null);\n // True from the start of stopRecording until it resolves. Distinct from\n // isPausedRef (user explicitly paused) — both freeze the duration tick,\n // but conflating them risks pauseRecording becoming a no-op while we're\n // stopping, or vice versa.\n const isStoppingRef = useRef<boolean>(false);\n\n // Shared duration update loop — starts a rAF loop that updates duration\n // from performance.now(). Used by both startRecording and resumeRecording.\n const startDurationLoop = useCallback(() => {\n const tick = () => {\n if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(tick);\n }\n };\n tick();\n }, []);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getGlobalContext();\n // Use addRecordingWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addRecordingWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n workletLoadedRef.current = true;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to load AudioWorklet module:', String(err));\n const error = new Error('Failed to load recording processor: ' + String(err));\n throw error;\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecordingRef.current) return;\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getGlobalContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Use the stream track's actual channel count from getSettings().\n // source.channelCount defaults to 2 per Web Audio spec (not the mic's\n // real count). Fall back to user-provided channelCount if unavailable.\n const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;\n if (detectedChannelCount === undefined) {\n console.warn(\n `[waveform-playlist] Could not detect stream channel count, using fallback: ${channelCount}`\n );\n }\n const streamChannelCount = detectedChannelCount ?? channelCount;\n\n const workletNode = context.createAudioWorkletNode('recording-processor', {\n channelCount: streamChannelCount,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Recording worklet processor error:', String(event));\n setError(new Error('Recording processor encountered an error'));\n };\n\n // Reset state before connecting — prevents race where a worklet message\n // arrives before refs are cleared, corrupting samplesProcessedBefore calculations\n recordedChunksRef.current = Array.from({ length: streamChannelCount }, () => []);\n totalSamplesRef.current = 0;\n setPeaks(Array.from({ length: streamChannelCount }, () => emptyPeaks(bits)));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n\n // Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { channels, done } = event.data as {\n channels: Float32Array[];\n done?: boolean;\n };\n\n // Empty messages can arrive from the final stop ack when nothing was buffered.\n // Handle channels first if present, then resolve the stop barrier.\n if (channels && channels.length > 0 && channels[0] && channels[0].length > 0) {\n for (let ch = 0; ch < channels.length; ch++) {\n if (!recordedChunksRef.current[ch]) {\n console.warn(\n `[waveform-playlist] Unexpected channel ${ch} from worklet (expected ${recordedChunksRef.current.length})`\n );\n recordedChunksRef.current[ch] = [];\n }\n recordedChunksRef.current[ch].push(channels[ch]);\n }\n // Capture sample offset before incrementing — used by peak alignment\n const samplesProcessedBefore = totalSamplesRef.current;\n totalSamplesRef.current += channels[0].length;\n\n // Skip live-preview peak updates while stop is in flight. The\n // queue may have many flushes pending; updating peaks for each\n // would let the live-preview waveform grow visibly past where\n // the user stopped, only to be replaced moments later by the\n // final clip (which is shorter due to latency compensation).\n // Chunks are already pushed above so no audio is lost.\n if (stopAckResolveRef.current === null) {\n setPeaks((prevPeaks) => {\n // Ensure we have an entry per channel\n const updated: (Int8Array | Int16Array)[] = [];\n for (let ch = 0; ch < channels.length; ch++) {\n const prev = prevPeaks[ch] ?? emptyPeaks(bits);\n updated.push(\n appendPeaks(prev, channels[ch], samplesPerPixel, samplesProcessedBefore, bits)\n );\n }\n return updated;\n });\n }\n }\n\n if (done) {\n stopAckResolveRef.current?.();\n stopAckResolveRef.current = null;\n }\n\n // Note: VU meter levels come from useMicrophoneLevel (meter-processor worklet)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Connect and start — after state reset and handler setup\n source.connect(workletNode);\n // Do NOT send sampleRate — worklet uses its global sampleRate (always correct)\n workletNode.port.postMessage({ command: 'start', channelCount: streamChannelCount });\n\n // Listen on MediaStreamTrack for mic unplug (MediaStream has no 'ended' event)\n const audioTrack = stream.getAudioTracks()[0] ?? null;\n audioTrackRef.current = audioTrack;\n if (audioTrack) {\n const onEnded = () => {\n console.warn('[waveform-playlist] Audio track ended (mic unplugged or revoked)');\n // Stop recording to clean up worklet, rAF loop, and UI state\n stopRecordingRef.current?.();\n setError(new Error('Microphone disconnected during recording'));\n };\n onTrackEndedRef.current = onEnded;\n audioTrack.addEventListener('ended', onEnded);\n }\n\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n startDurationLoop();\n } catch (err) {\n console.warn('[waveform-playlist] Failed to start recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecordingRef.current) {\n return null;\n }\n\n try {\n // Freeze the duration tick immediately so the live-preview width\n // (durationSamples = duration * sampleRate) doesn't keep growing\n // during the worklet stop handshake / queue drain. isStoppingRef\n // stops the rAF tick from rescheduling; cancelling animationFrameRef\n // kills any frame already queued.\n isStoppingRef.current = true;\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n audioTrackRef.current = null;\n onTrackEndedRef.current = null;\n }\n\n // Stop the worklet and wait for its final flush message before reading chunks.\n // Without the await, the partial buffer at the end of recording arrives after\n // the AudioBuffer is built and is silently dropped.\n // Snapshot the worklet node once — the drain loop yields the event loop\n // for up to 250ms, during which an unmount-cleanup could null the ref.\n const node = workletNodeRef.current;\n if (node) {\n const stopAck = new Promise<void>((resolve) => {\n stopAckResolveRef.current = resolve;\n });\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n // Safety timeout (1s) — purely a circuit breaker for a truly failed\n // worklet (crashed, context closed). The drain loop below catches\n // any straggler flush messages that were queued before the timeout\n // fired, so chunks are complete even when this fires.\n const timeout = new Promise<void>((resolve) => {\n timeoutId = setTimeout(resolve, 1000);\n });\n\n node.port.postMessage({ command: 'stop' });\n await Promise.race([stopAck, timeout]);\n clearTimeout(timeoutId);\n stopAckResolveRef.current = null;\n\n // Drain the event-loop queue. During recording the worklet posts a\n // flush every ~16ms; if main was slower than that, messages back up.\n // Yield repeatedly until totalSamplesRef stabilizes for several\n // consecutive ticks — at that point the queue is empty.\n let lastSamples = -1;\n let stable = 0;\n for (let i = 0; i < 50; i++) {\n if (totalSamplesRef.current === lastSamples) {\n if (++stable >= 3) break;\n } else {\n stable = 0;\n lastSamples = totalSamplesRef.current;\n }\n await new Promise<void>((r) => setTimeout(r, 5));\n }\n\n // Null the handler so any late delivery from this worklet doesn't\n // contaminate a subsequent recording session's chunks.\n node.port.onmessage = null;\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(node);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during stop:', String(err));\n }\n }\n node.disconnect();\n }\n\n // Create final AudioBuffer from accumulated per-channel chunks\n const context = getGlobalContext();\n const rawContext = context.rawContext as AudioContext;\n const numChannels = recordedChunksRef.current.length || channelCount;\n const channelData = recordedChunksRef.current.map((chunks) => concatenateAudioData(chunks));\n const totalSamples = channelData[0]?.length ?? 0;\n\n // Guard: if no samples were captured (e.g., stop called immediately after start),\n // return null instead of creating a 0-length AudioBuffer which throws\n if (totalSamples === 0) {\n console.warn('[waveform-playlist] Recording stopped with 0 samples captured — discarding');\n return null;\n }\n\n const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.warn('[waveform-playlist] Failed to stop recording:', String(err));\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n } finally {\n // Always clear recording flags, even on failure — otherwise the UI gets\n // stuck in \"recording\" state if AudioBuffer creation throws.\n isRecordingRef.current = false;\n isPausedRef.current = false;\n isStoppingRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n }\n }, [channelCount]);\n stopRecordingRef.current = stopRecording;\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n workletNodeRef.current?.port.postMessage({ command: 'pause' });\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n workletNodeRef.current?.port.postMessage({ command: 'resume' });\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n startDurationLoop();\n }\n }, [isRecording, isPaused, duration, startDurationLoop]);\n\n // Cleanup on unmount — read refs in cleanup, not effect body.\n // Pattern #16 (copy refs in effect body) doesn't apply to [] deps — refs are\n // null at mount and only set later during startRecording.\n useEffect(() => {\n return () => {\n // Remove mic-unplug listener\n if (audioTrackRef.current && onTrackEndedRef.current) {\n audioTrackRef.current.removeEventListener('ended', onTrackEndedRef.current);\n }\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (err) {\n console.warn('[waveform-playlist] Source disconnect during cleanup:', String(err));\n }\n }\n try {\n workletNodeRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Worklet disconnect during cleanup:', String(err));\n }\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(\n async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(err instanceof Error ? err : new Error('Failed to access microphone'));\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n },\n [stream, enumerateDevices]\n );\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state, enumerate devices, and listen for hot-plug changes\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Re-enumerate when devices are plugged in or removed\n navigator.mediaDevices.addEventListener('devicechange', enumerateDevices);\n\n // Cleanup on unmount\n return () => {\n navigator.mediaDevices.removeEventListener('devicechange', enumerateDevices);\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, [enumerateDevices, stream]);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses an AudioWorklet-based meter processor for sample-accurate\n * peak and RMS metering without requestAnimationFrame overhead.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { addMeterWorkletModule, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * Number of channels to meter (1 = mono, 2 = stereo)\n * Default: 1\n */\n channelCount?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current peak audio level (0-1)\n * For single channel: channel 0 level\n * For multi-channel: max across all channels\n */\n level: number;\n\n /**\n * Held peak level since last reset (0-1)\n * For single channel: channel 0 peak\n * For multi-channel: max across all channels\n */\n peakLevel: number;\n\n /**\n * Reset the held peak level\n */\n resetPeak: () => void;\n\n /**\n * Per-channel peak levels (0-1). Array length matches channelCount.\n * True peak: max absolute sample value per analysis frame.\n */\n levels: number[];\n\n /**\n * Per-channel held peak levels (0-1). Array length matches channelCount.\n */\n peakLevels: number[];\n\n /**\n * Per-channel RMS levels (0-1). Array length matches channelCount.\n * RMS: root mean square of samples per analysis frame.\n */\n rmsLevels: number[];\n\n /**\n * Error from meter setup (worklet load failure, context issues, etc.)\n * Null when metering is working normally.\n */\n error: Error | null;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current peak level, RMS level, and held peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { levels, rmsLevels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });\n *\n * return <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const { updateRate = 60, channelCount = 1 } = options;\n\n const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n const [meterError, setMeterError] = useState<Error | null>(null);\n\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n\n const resetPeak = useCallback(\n () => setPeakLevels(new Array(channelCount).fill(0)),\n [channelCount]\n );\n\n useEffect(() => {\n if (!stream) {\n setLevels(new Array(channelCount).fill(0));\n setPeakLevels(new Array(channelCount).fill(0));\n setRmsLevels(new Array(channelCount).fill(0));\n smoothedPeakRef.current = new Array(channelCount).fill(0);\n return;\n }\n\n let isMounted = true;\n\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n const context = getGlobalContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n if (!isMounted) return;\n\n // Auto-detect actual mic channel count from stream\n const trackSettings = stream.getAudioTracks()[0]?.getSettings();\n const actualChannels = trackSettings?.channelCount ?? channelCount;\n\n // Use addMeterWorkletModule with rawContext.audioWorklet.addModule callback.\n // Can't use Tone.js addAudioWorkletModule — it caches a single _workletPromise\n // per context, silently skipping subsequent URLs.\n const rawCtx = context.rawContext as AudioContext;\n await addMeterWorkletModule((url) => rawCtx.audioWorklet.addModule(url));\n if (!isMounted) return;\n\n // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n // in webpack-aliased environments (Docusaurus)\n const workletNode = context.createAudioWorkletNode('meter-processor', {\n channelCount: actualChannels,\n channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n processorOptions: {\n numberOfChannels: actualChannels,\n updateRate,\n },\n });\n workletNodeRef.current = workletNode;\n\n workletNode.onprocessorerror = (event) => {\n console.warn('[waveform-playlist] Mic meter worklet processor error:', String(event));\n };\n\n // Create source and connect: source → meter\n // Don't connect output to destination — mic monitoring would cause feedback\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n source.connect(workletNode);\n\n smoothedPeakRef.current = new Array(actualChannels).fill(0);\n\n // Listen for meter data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n if (!isMounted) return;\n\n const { peak, rms } = event.data as MeterMessage;\n const smoothed = smoothedPeakRef.current;\n\n const peakValues: number[] = [];\n const rmsValues: number[] = [];\n\n for (let ch = 0; ch < peak.length; ch++) {\n smoothed[ch] = Math.max(peak[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n peakValues.push(gainToNormalized(smoothed[ch]));\n rmsValues.push(gainToNormalized(rms[ch]));\n }\n\n // Mirror mono to fill requested channelCount\n const mirroredPeaks =\n peak.length < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;\n const mirroredRms =\n peak.length < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;\n\n setLevels(mirroredPeaks);\n setRmsLevels(mirroredRms);\n setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));\n };\n };\n\n setupMonitoring().catch((err) => {\n console.warn('[waveform-playlist] Failed to set up mic level monitoring:', String(err));\n if (isMounted) {\n setMeterError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return () => {\n isMounted = false;\n\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (err) {\n console.warn('[waveform-playlist] Mic source disconnect during cleanup:', String(err));\n }\n sourceRef.current = null;\n }\n\n if (workletNodeRef.current) {\n try {\n workletNodeRef.current.disconnect();\n workletNodeRef.current.port.close();\n } catch (err) {\n console.warn('[waveform-playlist] Mic meter disconnect during cleanup:', String(err));\n }\n workletNodeRef.current = null;\n }\n };\n }, [stream, updateRate, channelCount]);\n\n // Backwards-compatible scalar values\n const level = channelCount === 1 ? (levels[0] ?? 0) : Math.max(...levels);\n const peakLevel = channelCount === 1 ? (peakLevels[0] ?? 0) : Math.max(...peakLevels);\n\n return {\n level,\n peakLevel,\n resetPeak,\n levels,\n peakLevels,\n rmsLevels,\n error: meterError,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip, audibleLatencySamples } from '@waveform-playlist/core';\nimport {\n resumeGlobalAudioContext,\n getGlobalAudioContext,\n getGlobalContext,\n} from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n /** Per-channel peak levels (0-1). Array length matches channelCount. */\n levels: number[];\n /** Per-channel held peak levels (0-1). Array length matches channelCount. */\n peakLevels: number[];\n /** Per-channel RMS levels (0-1). Array length matches channelCount. */\n rmsLevels: number[];\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: (Int8Array | Int16Array)[];\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Capture timeline position when recording starts (not at stop time)\n const recordingStartTimeRef = useRef(0);\n\n // Keep selectedTrackId and currentTime in refs for use in callbacks.\n // Avoids stale closures and prevents 60fps useCallback recreation\n // (currentTime updates at animation-frame rate during playback).\n const selectedTrackIdRef = useRef(selectedTrackId);\n selectedTrackIdRef.current = selectedTrackId;\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n\n // Microphone access\n const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const {\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n resetPeak,\n error: meterError,\n } = useMicrophoneLevel(stream, {\n channelCount: recordingOptions.channelCount,\n });\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _recordedAudioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n // Reads selectedTrackId from ref to avoid stale closures when\n // auto-create track + start recording happen in the same render cycle\n const startRecording = useCallback(async () => {\n if (!selectedTrackIdRef.current) {\n setHookError(\n new Error('Cannot start recording: no track selected. Select or create a track first.')\n );\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n // Capture timeline position NOW — before recording starts.\n // Using currentTime at stop time would be wrong during overdub\n // (playback advances currentTime while recording).\n recordingStartTimeRef.current = currentTimeRef.current;\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n const trackId = selectedTrackIdRef.current;\n if (buffer && trackId) {\n const selectedTrackIndex = tracks.findIndex((t) => t.id === trackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${trackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.warn(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Use the captured start time (not live currentTime which advances during overdub)\n const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n const endSamples = selectedTrack.clips.map(\n (clip) => clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);\n\n // Latency compensation:\n // Two sources of delay between recording start and audible playback:\n // 1. Tone.js lookAhead (~100ms) — Transport schedules audio ahead of real time\n // 2. Output latency — hardware DAC delay before audio reaches speakers\n // The user hears playback delayed by both, so they perform late relative\n // to the timeline. Skip that duration at the start of the recorded audio.\n // Shared formula with the live preview in PlaylistVisualization — keep both\n // paths using `audibleLatencySamples` so trim widths match.\n const audioContext = getGlobalAudioContext();\n const outputLatency = audioContext.outputLatency ?? 0;\n const toneContext = getGlobalContext();\n const lookAhead = toneContext.lookAhead ?? 0;\n const latencyOffsetSamples = audibleLatencySamples(\n outputLatency,\n lookAhead,\n buffer.sampleRate\n );\n\n // Guard: very short recordings (< latency compensation) would produce negative duration\n const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);\n if (effectiveDuration === 0) {\n console.warn(\n '[waveform-playlist] Recording too short for latency compensation — discarding'\n );\n setHookError(new Error('Recording was too short to save. Try recording for longer.'));\n return;\n }\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: effectiveDuration,\n offsetSamples: latencyOffsetSamples,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [tracks, setTracks, stopRec]);\n\n // Auto-select first device when available, or fallback if selected device was unplugged\n useEffect(() => {\n if (!hasPermission || devices.length === 0) return;\n\n if (selectedDevice === null) {\n // First-time selection\n setSelectedDevice(devices[0].deviceId);\n } else if (!devices.some((d) => d.deviceId === selectedDevice)) {\n // Selected device was removed — fall back to first available\n const fallbackId = devices[0].deviceId;\n setSelectedDevice(fallbackId);\n resetPeak();\n requestAccess(fallbackId, audioConstraints);\n }\n }, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(\n async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n resetPeak();\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [requestAccess, audioConstraints, resetPeak]\n );\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n levels,\n peakLevels,\n rmsLevels,\n error: hookError || micError || meterError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n"],"mappings":"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","names":["error","useState","useEffect","useCallback","useEffect","useState","useRef","useCallback","getGlobalContext","useState","useCallback","useEffect","useRef","getGlobalContext","useState","useRef","useCallback","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveform-playlist/recording",
|
|
3
|
-
"version": "12.0
|
|
3
|
+
"version": "12.1.0",
|
|
4
4
|
"description": "Audio recording support for waveform-playlist using AudioWorklet",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -36,17 +36,21 @@
|
|
|
36
36
|
"README.md"
|
|
37
37
|
],
|
|
38
38
|
"devDependencies": {
|
|
39
|
+
"@testing-library/react": "^16.3.2",
|
|
39
40
|
"@types/react": "^18.2.45",
|
|
40
41
|
"@types/styled-components": "^5.1.26",
|
|
42
|
+
"jsdom": "^28.1.0",
|
|
43
|
+
"react": "^18.3.1",
|
|
44
|
+
"react-dom": "^18.3.1",
|
|
41
45
|
"tsup": "^8.0.1",
|
|
42
46
|
"typescript": "^5.3.3",
|
|
43
47
|
"vitest": "^3.0.0"
|
|
44
48
|
},
|
|
45
49
|
"dependencies": {
|
|
46
|
-
"@waveform-playlist/core": "12.
|
|
47
|
-
"@waveform-playlist/
|
|
48
|
-
"@waveform-playlist/
|
|
49
|
-
"@waveform-playlist/
|
|
50
|
+
"@waveform-playlist/core": "12.1.0",
|
|
51
|
+
"@waveform-playlist/worklets": "12.2.0",
|
|
52
|
+
"@waveform-playlist/playout": "12.3.0",
|
|
53
|
+
"@waveform-playlist/ui-components": "12.1.0"
|
|
50
54
|
},
|
|
51
55
|
"peerDependencies": {
|
|
52
56
|
"react": "^18.0.0",
|