@waveform-playlist/recording 5.3.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +150 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +147 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClipTrack } from '@waveform-playlist/core';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -133,6 +134,54 @@ interface UseMicrophoneLevelReturn {
|
|
|
133
134
|
*/
|
|
134
135
|
declare function useMicrophoneLevel(stream: MediaStream | null, options?: UseMicrophoneLevelOptions): UseMicrophoneLevelReturn;
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Hook for integrated multi-track recording
|
|
139
|
+
* Combines recording functionality with track management
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
interface IntegratedRecordingOptions {
|
|
143
|
+
/**
|
|
144
|
+
* Current playback/cursor position in seconds
|
|
145
|
+
* Recording will start from max(currentTime, lastClipEndTime)
|
|
146
|
+
*/
|
|
147
|
+
currentTime?: number;
|
|
148
|
+
/**
|
|
149
|
+
* MediaTrackConstraints for audio recording
|
|
150
|
+
* These will override the recording-optimized defaults (echo cancellation off, low latency)
|
|
151
|
+
*/
|
|
152
|
+
audioConstraints?: MediaTrackConstraints;
|
|
153
|
+
/**
|
|
154
|
+
* Number of channels to record (1 = mono, 2 = stereo)
|
|
155
|
+
* Default: 1 (mono)
|
|
156
|
+
*/
|
|
157
|
+
channelCount?: number;
|
|
158
|
+
/**
|
|
159
|
+
* Samples per pixel for peak generation
|
|
160
|
+
* Default: 1024
|
|
161
|
+
*/
|
|
162
|
+
samplesPerPixel?: number;
|
|
163
|
+
}
|
|
164
|
+
interface UseIntegratedRecordingReturn {
|
|
165
|
+
isRecording: boolean;
|
|
166
|
+
isPaused: boolean;
|
|
167
|
+
duration: number;
|
|
168
|
+
level: number;
|
|
169
|
+
peakLevel: number;
|
|
170
|
+
error: Error | null;
|
|
171
|
+
stream: MediaStream | null;
|
|
172
|
+
devices: MicrophoneDevice[];
|
|
173
|
+
hasPermission: boolean;
|
|
174
|
+
selectedDevice: string | null;
|
|
175
|
+
startRecording: () => void;
|
|
176
|
+
stopRecording: () => void;
|
|
177
|
+
pauseRecording: () => void;
|
|
178
|
+
resumeRecording: () => void;
|
|
179
|
+
requestMicAccess: () => Promise<void>;
|
|
180
|
+
changeDevice: (deviceId: string) => Promise<void>;
|
|
181
|
+
recordingPeaks: Int8Array | Int16Array;
|
|
182
|
+
}
|
|
183
|
+
declare function useIntegratedRecording(tracks: ClipTrack[], setTracks: (tracks: ClipTrack[]) => void, selectedTrackId: string | null, options?: IntegratedRecordingOptions): UseIntegratedRecordingReturn;
|
|
184
|
+
|
|
136
185
|
/**
|
|
137
186
|
* RecordButton - Control button for starting/stopping recording
|
|
138
187
|
*/
|
|
@@ -231,4 +280,4 @@ declare function concatenateAudioData(chunks: Float32Array[]): Float32Array;
|
|
|
231
280
|
*/
|
|
232
281
|
declare function createAudioBuffer(audioContext: AudioContext, samples: Float32Array, sampleRate: number, channelCount?: number): AudioBuffer;
|
|
233
282
|
|
|
234
|
-
export { type MicrophoneDevice, MicrophoneSelector, type MicrophoneSelectorProps, RecordButton, type RecordButtonProps, type RecordingData, RecordingIndicator, type RecordingIndicatorProps, type RecordingOptions, type RecordingState, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, VUMeter, type VUMeterProps, concatenateAudioData, createAudioBuffer, generatePeaks, useMicrophoneAccess, useMicrophoneLevel, useRecording };
|
|
283
|
+
export { type IntegratedRecordingOptions, type MicrophoneDevice, MicrophoneSelector, type MicrophoneSelectorProps, RecordButton, type RecordButtonProps, type RecordingData, RecordingIndicator, type RecordingIndicatorProps, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, VUMeter, type VUMeterProps, concatenateAudioData, createAudioBuffer, generatePeaks, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClipTrack } from '@waveform-playlist/core';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -133,6 +134,54 @@ interface UseMicrophoneLevelReturn {
|
|
|
133
134
|
*/
|
|
134
135
|
declare function useMicrophoneLevel(stream: MediaStream | null, options?: UseMicrophoneLevelOptions): UseMicrophoneLevelReturn;
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Hook for integrated multi-track recording
|
|
139
|
+
* Combines recording functionality with track management
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
interface IntegratedRecordingOptions {
|
|
143
|
+
/**
|
|
144
|
+
* Current playback/cursor position in seconds
|
|
145
|
+
* Recording will start from max(currentTime, lastClipEndTime)
|
|
146
|
+
*/
|
|
147
|
+
currentTime?: number;
|
|
148
|
+
/**
|
|
149
|
+
* MediaTrackConstraints for audio recording
|
|
150
|
+
* These will override the recording-optimized defaults (echo cancellation off, low latency)
|
|
151
|
+
*/
|
|
152
|
+
audioConstraints?: MediaTrackConstraints;
|
|
153
|
+
/**
|
|
154
|
+
* Number of channels to record (1 = mono, 2 = stereo)
|
|
155
|
+
* Default: 1 (mono)
|
|
156
|
+
*/
|
|
157
|
+
channelCount?: number;
|
|
158
|
+
/**
|
|
159
|
+
* Samples per pixel for peak generation
|
|
160
|
+
* Default: 1024
|
|
161
|
+
*/
|
|
162
|
+
samplesPerPixel?: number;
|
|
163
|
+
}
|
|
164
|
+
interface UseIntegratedRecordingReturn {
|
|
165
|
+
isRecording: boolean;
|
|
166
|
+
isPaused: boolean;
|
|
167
|
+
duration: number;
|
|
168
|
+
level: number;
|
|
169
|
+
peakLevel: number;
|
|
170
|
+
error: Error | null;
|
|
171
|
+
stream: MediaStream | null;
|
|
172
|
+
devices: MicrophoneDevice[];
|
|
173
|
+
hasPermission: boolean;
|
|
174
|
+
selectedDevice: string | null;
|
|
175
|
+
startRecording: () => void;
|
|
176
|
+
stopRecording: () => void;
|
|
177
|
+
pauseRecording: () => void;
|
|
178
|
+
resumeRecording: () => void;
|
|
179
|
+
requestMicAccess: () => Promise<void>;
|
|
180
|
+
changeDevice: (deviceId: string) => Promise<void>;
|
|
181
|
+
recordingPeaks: Int8Array | Int16Array;
|
|
182
|
+
}
|
|
183
|
+
declare function useIntegratedRecording(tracks: ClipTrack[], setTracks: (tracks: ClipTrack[]) => void, selectedTrackId: string | null, options?: IntegratedRecordingOptions): UseIntegratedRecordingReturn;
|
|
184
|
+
|
|
136
185
|
/**
|
|
137
186
|
* RecordButton - Control button for starting/stopping recording
|
|
138
187
|
*/
|
|
@@ -231,4 +280,4 @@ declare function concatenateAudioData(chunks: Float32Array[]): Float32Array;
|
|
|
231
280
|
*/
|
|
232
281
|
declare function createAudioBuffer(audioContext: AudioContext, samples: Float32Array, sampleRate: number, channelCount?: number): AudioBuffer;
|
|
233
282
|
|
|
234
|
-
export { type MicrophoneDevice, MicrophoneSelector, type MicrophoneSelectorProps, RecordButton, type RecordButtonProps, type RecordingData, RecordingIndicator, type RecordingIndicatorProps, type RecordingOptions, type RecordingState, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, VUMeter, type VUMeterProps, concatenateAudioData, createAudioBuffer, generatePeaks, useMicrophoneAccess, useMicrophoneLevel, useRecording };
|
|
283
|
+
export { type IntegratedRecordingOptions, type MicrophoneDevice, MicrophoneSelector, type MicrophoneSelectorProps, RecordButton, type RecordButtonProps, type RecordingData, RecordingIndicator, type RecordingIndicatorProps, type RecordingOptions, type RecordingState, type UseIntegratedRecordingReturn, type UseMicrophoneAccessReturn, type UseMicrophoneLevelOptions, type UseMicrophoneLevelReturn, type UseRecordingReturn, VUMeter, type VUMeterProps, concatenateAudioData, createAudioBuffer, generatePeaks, useIntegratedRecording, useMicrophoneAccess, useMicrophoneLevel, useRecording };
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ __export(src_exports, {
|
|
|
37
37
|
concatenateAudioData: () => concatenateAudioData,
|
|
38
38
|
createAudioBuffer: () => createAudioBuffer,
|
|
39
39
|
generatePeaks: () => generatePeaks,
|
|
40
|
+
useIntegratedRecording: () => useIntegratedRecording,
|
|
40
41
|
useMicrophoneAccess: () => useMicrophoneAccess,
|
|
41
42
|
useMicrophoneLevel: () => useMicrophoneLevel,
|
|
42
43
|
useRecording: () => useRecording
|
|
@@ -484,6 +485,152 @@ function useMicrophoneLevel(stream, options = {}) {
|
|
|
484
485
|
};
|
|
485
486
|
}
|
|
486
487
|
|
|
488
|
+
// src/hooks/useIntegratedRecording.ts
|
|
489
|
+
var import_react4 = require("react");
|
|
490
|
+
var import_playout = require("@waveform-playlist/playout");
|
|
491
|
+
function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}) {
|
|
492
|
+
const { currentTime = 0, audioConstraints, ...recordingOptions } = options;
|
|
493
|
+
const [isMonitoring, setIsMonitoring] = (0, import_react4.useState)(false);
|
|
494
|
+
const [selectedDevice, setSelectedDevice] = (0, import_react4.useState)(null);
|
|
495
|
+
const [hookError, setHookError] = (0, import_react4.useState)(null);
|
|
496
|
+
const {
|
|
497
|
+
stream,
|
|
498
|
+
devices,
|
|
499
|
+
hasPermission,
|
|
500
|
+
requestAccess,
|
|
501
|
+
error: micError
|
|
502
|
+
} = useMicrophoneAccess();
|
|
503
|
+
const { level, peakLevel } = useMicrophoneLevel(stream);
|
|
504
|
+
const {
|
|
505
|
+
isRecording,
|
|
506
|
+
isPaused,
|
|
507
|
+
duration,
|
|
508
|
+
peaks,
|
|
509
|
+
audioBuffer: _audioBuffer,
|
|
510
|
+
startRecording: startRec,
|
|
511
|
+
stopRecording: stopRec,
|
|
512
|
+
pauseRecording,
|
|
513
|
+
resumeRecording,
|
|
514
|
+
error: recError
|
|
515
|
+
} = useRecording(stream, recordingOptions);
|
|
516
|
+
const startRecording = (0, import_react4.useCallback)(async () => {
|
|
517
|
+
if (!selectedTrackId) {
|
|
518
|
+
setHookError(new Error("Cannot start recording: no track selected. Select or create a track first."));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
setHookError(null);
|
|
523
|
+
if (!isMonitoring) {
|
|
524
|
+
await (0, import_playout.resumeGlobalAudioContext)();
|
|
525
|
+
setIsMonitoring(true);
|
|
526
|
+
}
|
|
527
|
+
await startRec();
|
|
528
|
+
} catch (err) {
|
|
529
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
530
|
+
}
|
|
531
|
+
}, [selectedTrackId, isMonitoring, startRec]);
|
|
532
|
+
const stopRecording = (0, import_react4.useCallback)(async () => {
|
|
533
|
+
let buffer;
|
|
534
|
+
try {
|
|
535
|
+
buffer = await stopRec();
|
|
536
|
+
} catch (err) {
|
|
537
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (buffer && selectedTrackId) {
|
|
541
|
+
const selectedTrackIndex = tracks.findIndex((t) => t.id === selectedTrackId);
|
|
542
|
+
if (selectedTrackIndex === -1) {
|
|
543
|
+
const err = new Error(
|
|
544
|
+
`Recording completed but track "${selectedTrackId}" no longer exists. The recorded audio could not be saved.`
|
|
545
|
+
);
|
|
546
|
+
console.error(`[waveform-playlist] ${err.message}`);
|
|
547
|
+
setHookError(err);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const selectedTrack = tracks[selectedTrackIndex];
|
|
551
|
+
const currentTimeSamples = Math.floor(currentTime * buffer.sampleRate);
|
|
552
|
+
let lastClipEndSample = 0;
|
|
553
|
+
if (selectedTrack.clips.length > 0) {
|
|
554
|
+
const endSamples = selectedTrack.clips.map(
|
|
555
|
+
(clip) => clip.startSample + clip.durationSamples
|
|
556
|
+
);
|
|
557
|
+
lastClipEndSample = Math.max(...endSamples);
|
|
558
|
+
}
|
|
559
|
+
const startSample = Math.max(currentTimeSamples, lastClipEndSample);
|
|
560
|
+
const newClip = {
|
|
561
|
+
id: `clip-${Date.now()}`,
|
|
562
|
+
audioBuffer: buffer,
|
|
563
|
+
startSample,
|
|
564
|
+
durationSamples: buffer.length,
|
|
565
|
+
offsetSamples: 0,
|
|
566
|
+
sampleRate: buffer.sampleRate,
|
|
567
|
+
sourceDurationSamples: buffer.length,
|
|
568
|
+
gain: 1,
|
|
569
|
+
name: `Recording ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`
|
|
570
|
+
};
|
|
571
|
+
const newTracks = tracks.map((track, index) => {
|
|
572
|
+
if (index === selectedTrackIndex) {
|
|
573
|
+
return {
|
|
574
|
+
...track,
|
|
575
|
+
clips: [...track.clips, newClip]
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return track;
|
|
579
|
+
});
|
|
580
|
+
setTracks(newTracks);
|
|
581
|
+
}
|
|
582
|
+
}, [selectedTrackId, tracks, setTracks, currentTime, stopRec]);
|
|
583
|
+
(0, import_react4.useEffect)(() => {
|
|
584
|
+
if (hasPermission && devices.length > 0 && selectedDevice === null) {
|
|
585
|
+
setSelectedDevice(devices[0].deviceId);
|
|
586
|
+
}
|
|
587
|
+
}, [hasPermission, devices.length]);
|
|
588
|
+
const requestMicAccess = (0, import_react4.useCallback)(async () => {
|
|
589
|
+
try {
|
|
590
|
+
setHookError(null);
|
|
591
|
+
await requestAccess(void 0, audioConstraints);
|
|
592
|
+
await (0, import_playout.resumeGlobalAudioContext)();
|
|
593
|
+
setIsMonitoring(true);
|
|
594
|
+
} catch (err) {
|
|
595
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
596
|
+
}
|
|
597
|
+
}, [requestAccess, audioConstraints]);
|
|
598
|
+
const changeDevice = (0, import_react4.useCallback)(async (deviceId) => {
|
|
599
|
+
try {
|
|
600
|
+
setHookError(null);
|
|
601
|
+
setSelectedDevice(deviceId);
|
|
602
|
+
await requestAccess(deviceId, audioConstraints);
|
|
603
|
+
await (0, import_playout.resumeGlobalAudioContext)();
|
|
604
|
+
setIsMonitoring(true);
|
|
605
|
+
} catch (err) {
|
|
606
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
607
|
+
}
|
|
608
|
+
}, [requestAccess, audioConstraints]);
|
|
609
|
+
return {
|
|
610
|
+
// Recording state
|
|
611
|
+
isRecording,
|
|
612
|
+
isPaused,
|
|
613
|
+
duration,
|
|
614
|
+
level,
|
|
615
|
+
peakLevel,
|
|
616
|
+
error: hookError || micError || recError,
|
|
617
|
+
// Microphone state
|
|
618
|
+
stream,
|
|
619
|
+
devices,
|
|
620
|
+
hasPermission,
|
|
621
|
+
selectedDevice,
|
|
622
|
+
// Recording controls
|
|
623
|
+
startRecording,
|
|
624
|
+
stopRecording,
|
|
625
|
+
pauseRecording,
|
|
626
|
+
resumeRecording,
|
|
627
|
+
requestMicAccess,
|
|
628
|
+
changeDevice,
|
|
629
|
+
// Track state
|
|
630
|
+
recordingPeaks: peaks
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
487
634
|
// src/components/RecordButton.tsx
|
|
488
635
|
var import_styled_components = __toESM(require("styled-components"));
|
|
489
636
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -662,7 +809,7 @@ var RecordingIndicator2 = ({
|
|
|
662
809
|
};
|
|
663
810
|
|
|
664
811
|
// src/components/VUMeter.tsx
|
|
665
|
-
var
|
|
812
|
+
var import_react5 = __toESM(require("react"));
|
|
666
813
|
var import_styled_components4 = __toESM(require("styled-components"));
|
|
667
814
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
668
815
|
var MeterContainer = import_styled_components4.default.div`
|
|
@@ -739,7 +886,7 @@ var VUMeterComponent = ({
|
|
|
739
886
|
] })
|
|
740
887
|
] });
|
|
741
888
|
};
|
|
742
|
-
var VUMeter =
|
|
889
|
+
var VUMeter = import_react5.default.memo(VUMeterComponent);
|
|
743
890
|
// Annotate the CommonJS export names for ESM import in node:
|
|
744
891
|
0 && (module.exports = {
|
|
745
892
|
MicrophoneSelector,
|
|
@@ -749,6 +896,7 @@ var VUMeter = import_react4.default.memo(VUMeterComponent);
|
|
|
749
896
|
concatenateAudioData,
|
|
750
897
|
createAudioBuffer,
|
|
751
898
|
generatePeaks,
|
|
899
|
+
useIntegratedRecording,
|
|
752
900
|
useMicrophoneAccess,
|
|
753
901
|
useMicrophoneLevel,
|
|
754
902
|
useRecording
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/components/RecordButton.tsx","../src/components/MicrophoneSelector.tsx","../src/components/RecordingIndicator.tsx","../src/components/VUMeter.tsx"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport { useRecording, useMicrophoneAccess, useMicrophoneLevel } from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n} from './hooks';\n\n// Components\nexport { RecordButton, MicrophoneSelector, RecordingIndicator, VUMeter } from './components';\nexport type {\n RecordButtonProps,\n MicrophoneSelectorProps,\n RecordingIndicatorProps,\n VUMeterProps,\n} from './components';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n\n// Utilities\nexport { generatePeaks } from './utils/peaksGenerator';\nexport { createAudioBuffer, concatenateAudioData } from './utils/audioBufferUtils';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getContext } from 'tone';\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const {\n channelCount = 1,\n samplesPerPixel = 1024,\n } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [peaks, setPeaks] = useState<Int8Array | Int16Array>(new Int16Array(0));\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n const bits: 8 | 16 = 16; // Match the bit depth used by the final waveform\n\n // Global flag to prevent loading worklet multiple times\n // (AudioWorklet processors can only be registered once per AudioContext)\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const recordedChunksRef = useRef<Float32Array[]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getContext();\n // Load the worklet module\n // Use a relative path that works when bundled\n const workletUrl = new URL(\n './worklet/recording-processor.worklet.js',\n import.meta.url\n ).href;\n\n // Use Tone's addAudioWorkletModule for cross-browser compatibility\n await context.addAudioWorkletModule(workletUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.error('Failed to load AudioWorklet module:', err);\n throw new Error('Failed to load recording processor');\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Create AudioWorklet node using Tone's method\n const workletNode = context.createAudioWorkletNode('recording-processor');\n workletNodeRef.current = workletNode;\n\n // Connect source to worklet (but not to destination - no monitoring)\n source.connect(workletNode);\n\n //Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { samples } = event.data;\n\n // Accumulate samples\n recordedChunksRef.current.push(samples);\n totalSamplesRef.current += samples.length;\n\n // Update peaks incrementally for live waveform visualization\n setPeaks((prevPeaks) =>\n appendPeaks(\n prevPeaks,\n samples,\n samplesPerPixel,\n totalSamplesRef.current - samples.length,\n bits\n )\n );\n\n // Note: VU meter levels come from useMicrophoneLevel (AnalyserNode)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Start the worklet processor\n workletNode.port.postMessage({\n command: 'start',\n sampleRate: context.sampleRate,\n channelCount,\n });\n\n // Reset state\n recordedChunksRef.current = [];\n totalSamplesRef.current = 0;\n setPeaks(new Int16Array(0));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n\n // Start duration update loop\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n } catch (err) {\n console.error('Failed to start recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, loadWorklet, isRecording, isPaused]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecording) {\n return null;\n }\n\n try {\n // Stop the worklet\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (e) {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated chunks\n const allSamples = concatenateAudioData(recordedChunksRef.current);\n const context = getContext();\n // Use rawContext for createBuffer (native AudioContext method)\n const rawContext = context.rawContext as AudioContext;\n const buffer = createAudioBuffer(\n rawContext,\n allSamples,\n rawContext.sampleRate,\n channelCount\n );\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.error('Failed to stop recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n }\n }, [isRecording, channelCount]);\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n }\n }, [isRecording, isPaused, duration]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (e) {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert Float32Array to AudioBuffer\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n samples: Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n const buffer = audioContext.createBuffer(\n channelCount,\n samples.length,\n sampleRate\n );\n\n // Copy samples to buffer (for now, just mono)\n // Create a new Float32Array to ensure correct type\n const typedSamples = new Float32Array(samples);\n buffer.copyToChannel(typedSamples, 0);\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, newSamples, sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, combined, sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n peakArray[i * 2] = Math.floor(min * maxValue);\n peakArray[i * 2 + 1] = Math.floor(max * maxValue);\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.floor(min * maxValue);\n updated[existingPeaks.length - 1] = Math.floor(max * maxValue);\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(\n err instanceof Error ? err : new Error('Failed to access microphone')\n );\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n }, [stream, enumerateDevices]);\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state and enumerate devices\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Cleanup on unmount\n return () => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, []);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses Tone.js Meter for real-time audio level monitoring.\n */\n\nimport { useEffect, useState, useRef } from 'react';\nimport { Meter, getContext, connect } from 'tone';\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * FFT size for the analyser\n * Default: 256\n */\n fftSize?: number;\n\n /**\n * Smoothing time constant (0-1)\n * Higher values = smoother but slower response\n * Default: 0.8\n */\n smoothingTimeConstant?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current audio level (0-1)\n * 0 = silence, 1 = maximum level\n */\n level: number;\n\n /**\n * Peak level since last reset (0-1)\n */\n peakLevel: number;\n\n /**\n * Reset the peak level\n */\n resetPeak: () => void;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current level and peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { level, peakLevel, resetPeak } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const {\n updateRate = 60,\n smoothingTimeConstant = 0.8,\n } = options;\n\n const [level, setLevel] = useState(0);\n const [peakLevel, setPeakLevel] = useState(0);\n\n const meterRef = useRef<Meter | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const animationFrameRef = useRef<number | null>(null);\n\n const resetPeak = () => setPeakLevel(0);\n\n useEffect(() => {\n if (!stream) {\n setLevel(0);\n setPeakLevel(0);\n return;\n }\n\n let isMounted = true;\n\n // Setup audio monitoring\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n // Get Tone's context and resume if needed\n const context = getContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n if (!isMounted) return;\n\n // Create Tone.js Meter for level monitoring\n // Pass context to ensure it's created in the same context as the source\n const meter = new Meter({ smoothing: smoothingTimeConstant, context });\n meterRef.current = meter;\n\n // Create MediaStreamSource from the SAME context as the meter\n // Note: This creates a separate source from useRecording, but that's OK\n // since we're only using it for level monitoring (not recording)\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n\n // Connect source to meter using Tone's connect function\n connect(source, meter);\n\n // Start level monitoring\n const updateInterval = 1000 / updateRate;\n let lastUpdateTime = 0;\n\n const updateLevel = (timestamp: number) => {\n if (!isMounted || !meterRef.current) return;\n\n if (timestamp - lastUpdateTime >= updateInterval) {\n lastUpdateTime = timestamp;\n\n // Meter.getValue() returns dB, convert to 0-1 range\n const db = meterRef.current.getValue();\n const dbValue = typeof db === 'number' ? db : db[0];\n // dB is typically -Infinity to 0, map -100dB..0dB to 0..1\n // Using -100dB as floor since Firefox seems to report lower values\n const normalized = Math.max(0, Math.min(1, (dbValue + 100) / 100));\n\n setLevel(normalized);\n setPeakLevel(prev => Math.max(prev, normalized));\n }\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n setupMonitoring();\n\n // Cleanup\n return () => {\n isMounted = false;\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Disconnect and clean up\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (e) {\n // Ignore disconnect errors\n }\n sourceRef.current = null;\n }\n\n if (meterRef.current) {\n meterRef.current.dispose();\n meterRef.current = null;\n }\n };\n }, [stream, smoothingTimeConstant, updateRate]);\n\n return {\n level,\n peakLevel,\n resetPeak,\n };\n}\n","/**\n * RecordButton - Control button for starting/stopping recording\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordButtonProps {\n isRecording: boolean;\n onClick: () => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Button = styled.button<{ $isRecording: boolean }>`\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border: none;\n border-radius: 0.25rem;\n cursor: pointer;\n transition: all 0.2s ease-in-out;\n background: ${(props) => (props.$isRecording ? '#dc3545' : '#e74c3c')};\n color: white;\n\n &:hover:not(:disabled) {\n background: ${(props) => (props.$isRecording ? '#c82333' : '#c0392b')};\n transform: translateY(-1px);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n &:focus {\n outline: none;\n box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.3);\n }\n`;\n\nconst RecordingIndicator = styled.span`\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: white;\n margin-right: 0.5rem;\n animation: pulse 1.5s ease-in-out infinite;\n\n @keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n`;\n\nexport const RecordButton: React.FC<RecordButtonProps> = ({\n isRecording,\n onClick,\n disabled = false,\n className,\n}) => {\n return (\n <Button\n $isRecording={isRecording}\n onClick={onClick}\n disabled={disabled}\n className={className}\n aria-label={isRecording ? 'Stop recording' : 'Start recording'}\n >\n {isRecording && <RecordingIndicator />}\n {isRecording ? 'Stop Recording' : 'Record'}\n </Button>\n );\n};\n","/**\n * MicrophoneSelector - Dropdown for selecting microphone input device\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\nimport { BaseSelect, BaseLabel } from '@waveform-playlist/ui-components';\nimport { MicrophoneDevice } from '../types';\n\nexport interface MicrophoneSelectorProps {\n devices: MicrophoneDevice[];\n selectedDeviceId?: string;\n onDeviceChange: (deviceId: string) => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Select = styled(BaseSelect)`\n min-width: 200px;\n`;\n\nconst Label = styled(BaseLabel)`\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n`;\n\nexport const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = ({\n devices,\n selectedDeviceId,\n onDeviceChange,\n disabled = false,\n className,\n}) => {\n const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {\n onDeviceChange(event.target.value);\n };\n\n // Use first device if no selection provided\n const currentValue = selectedDeviceId || (devices.length > 0 ? devices[0].deviceId : '');\n\n return (\n <Label className={className}>\n Microphone\n <Select\n value={currentValue}\n onChange={handleChange}\n disabled={disabled || devices.length === 0}\n >\n {devices.length === 0 ? (\n <option value=\"\">No microphones found</option>\n ) : (\n devices.map((device) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))\n )}\n </Select>\n </Label>\n );\n};\n","/**\n * RecordingIndicator - Shows recording status, duration, and visual indicator\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordingIndicatorProps {\n isRecording: boolean;\n isPaused?: boolean;\n duration: number; // in seconds\n formatTime?: (seconds: number) => string;\n className?: string;\n}\n\nconst Container = styled.div<{ $isRecording: boolean }>`\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.5rem 0.75rem;\n background: ${(props) => (props.$isRecording ? '#fff3cd' : 'transparent')};\n border-radius: 0.25rem;\n transition: background 0.2s ease-in-out;\n`;\n\nconst Dot = styled.div<{ $isRecording: boolean; $isPaused: boolean }>`\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n opacity: ${(props) => (props.$isRecording ? 1 : 0)};\n transition: opacity 0.2s ease-in-out;\n\n ${(props) =>\n props.$isRecording &&\n !props.$isPaused &&\n `\n animation: blink 1.5s ease-in-out infinite;\n\n @keyframes blink {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n `}\n`;\n\nconst Duration = styled.span`\n font-family: 'Courier New', Monaco, monospace;\n font-size: 1rem;\n font-weight: 600;\n color: #495057;\n min-width: 70px;\n`;\n\nconst Status = styled.span<{ $isPaused: boolean }>`\n font-size: 0.75rem;\n font-weight: 500;\n color: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n text-transform: uppercase;\n`;\n\nconst defaultFormatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n};\n\nexport const RecordingIndicator: React.FC<RecordingIndicatorProps> = ({\n isRecording,\n isPaused = false,\n duration,\n formatTime = defaultFormatTime,\n className,\n}) => {\n return (\n <Container $isRecording={isRecording} className={className}>\n <Dot $isRecording={isRecording} $isPaused={isPaused} />\n <Duration>{formatTime(duration)}</Duration>\n {isRecording && <Status $isPaused={isPaused}>{isPaused ? 'Paused' : 'Recording'}</Status>}\n </Container>\n );\n};\n","/**\n * VU Meter Component\n *\n * Displays real-time audio input levels with color-coded zones\n * and peak indicator.\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface VUMeterProps {\n /**\n * Current audio level (0-1)\n */\n level: number;\n\n /**\n * Peak level (0-1)\n * Optional - if provided, shows peak indicator\n */\n peakLevel?: number;\n\n /**\n * Width of the meter in pixels\n * Default: 200\n */\n width?: number;\n\n /**\n * Height of the meter in pixels\n * Default: 20\n */\n height?: number;\n\n /**\n * Additional CSS class name\n */\n className?: string;\n}\n\nconst MeterContainer = styled.div<{ $width: number; $height: number }>`\n position: relative;\n width: ${props => props.$width}px;\n height: ${props => props.$height}px;\n background: #2c3e50;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);\n`;\n\n// Helper to get gradient color based on level\nconst getLevelGradient = (level: number): string => {\n if (level < 0.6) return 'linear-gradient(90deg, #27ae60, #2ecc71)';\n if (level < 0.85) return 'linear-gradient(90deg, #f39c12, #f1c40f)';\n return 'linear-gradient(90deg, #c0392b, #e74c3c)';\n};\n\n// Use .attrs() for frequently changing styles to avoid generating new CSS classes\nconst MeterFill = styled.div.attrs<{ $level: number; $height: number }>(props => ({\n style: {\n width: `${props.$level * 100}%`,\n height: `${props.$height}px`,\n background: getLevelGradient(props.$level),\n boxShadow: props.$level > 0.01 ? '0 0 8px rgba(255, 255, 255, 0.3)' : 'none',\n },\n}))<{ $level: number; $height: number }>`\n position: absolute;\n left: 0;\n top: 0;\n transition: width 0.05s ease-out, background 0.1s ease-out;\n`;\n\n// Use .attrs() for frequently changing left position\nconst PeakIndicator = styled.div.attrs<{ $peakLevel: number; $height: number }>(props => ({\n style: {\n left: `${props.$peakLevel * 100}%`,\n height: `${props.$height}px`,\n },\n}))<{ $peakLevel: number; $height: number }>`\n position: absolute;\n top: 0;\n width: 2px;\n background: #ecf0f1;\n box-shadow: 0 0 4px rgba(236, 240, 241, 0.8);\n transition: left 0.1s ease-out;\n`;\n\nconst ScaleMarkers = styled.div<{ $height: number }>`\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: ${props => props.$height}px;\n pointer-events: none;\n`;\n\nconst ScaleMark = styled.div<{ $position: number; $height: number }>`\n position: absolute;\n left: ${props => props.$position}%;\n top: 0;\n width: 1px;\n height: ${props => props.$height}px;\n background: rgba(255, 255, 255, 0.2);\n`;\n\n/**\n * VU Meter component for displaying audio input levels\n *\n * @example\n * ```typescript\n * import { useMicrophoneLevel, VUMeter } from '@waveform-playlist/recording';\n *\n * const { level, peakLevel } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} width={300} height={24} />;\n * ```\n */\nconst VUMeterComponent: React.FC<VUMeterProps> = ({\n level,\n peakLevel,\n width = 200,\n height = 20,\n className,\n}) => {\n // Clamp values to 0-1 range\n const clampedLevel = Math.max(0, Math.min(1, level));\n const clampedPeak = peakLevel !== undefined\n ? Math.max(0, Math.min(1, peakLevel))\n : 0;\n\n return (\n <MeterContainer $width={width} $height={height} className={className}>\n <MeterFill $level={clampedLevel} $height={height} />\n\n {peakLevel !== undefined && clampedPeak > 0 && (\n <PeakIndicator $peakLevel={clampedPeak} $height={height} />\n )}\n\n <ScaleMarkers $height={height}>\n <ScaleMark $position={60} $height={height} />\n <ScaleMark $position={85} $height={height} />\n </ScaleMarkers>\n </MeterContainer>\n );\n};\n\n// Memoize to prevent unnecessary re-renders when parent updates\nexport const VUMeter = React.memo(VUMeterComponent);\n"],"mappings":"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","names":["RecordingIndicator","newPeaks","result","import_react","import_react","import_tone","styled","import_styled_components","import_jsx_runtime","styled","import_styled_components","import_jsx_runtime","styled","RecordingIndicator","import_react","import_styled_components","import_jsx_runtime","styled","React"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts","../src/components/RecordButton.tsx","../src/components/MicrophoneSelector.tsx","../src/components/RecordingIndicator.tsx","../src/components/VUMeter.tsx"],"sourcesContent":["/**\n * @waveform-playlist/recording\n *\n * Audio recording support using AudioWorklet for waveform-playlist\n */\n\n// Hooks\nexport { useRecording, useMicrophoneAccess, useMicrophoneLevel, useIntegratedRecording } from './hooks';\nexport type {\n UseMicrophoneLevelOptions,\n UseMicrophoneLevelReturn,\n UseIntegratedRecordingReturn,\n IntegratedRecordingOptions,\n} from './hooks';\n\n// Components\nexport { RecordButton, MicrophoneSelector, RecordingIndicator, VUMeter } from './components';\nexport type {\n RecordButtonProps,\n MicrophoneSelectorProps,\n RecordingIndicatorProps,\n VUMeterProps,\n} from './components';\n\n// Types\nexport type {\n RecordingState,\n RecordingData,\n MicrophoneDevice,\n RecordingOptions,\n UseRecordingReturn,\n UseMicrophoneAccessReturn,\n} from './types';\n\n// Utilities\nexport { generatePeaks } from './utils/peaksGenerator';\nexport { createAudioBuffer, concatenateAudioData } from './utils/audioBufferUtils';\n","/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getContext } from 'tone';\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const {\n channelCount = 1,\n samplesPerPixel = 1024,\n } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [peaks, setPeaks] = useState<Int8Array | Int16Array>(new Int16Array(0));\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n const bits: 8 | 16 = 16; // Match the bit depth used by the final waveform\n\n // Global flag to prevent loading worklet multiple times\n // (AudioWorklet processors can only be registered once per AudioContext)\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const recordedChunksRef = useRef<Float32Array[]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getContext();\n // Load the worklet module\n // Use a relative path that works when bundled\n const workletUrl = new URL(\n './worklet/recording-processor.worklet.js',\n import.meta.url\n ).href;\n\n // Use Tone's addAudioWorkletModule for cross-browser compatibility\n await context.addAudioWorkletModule(workletUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.error('Failed to load AudioWorklet module:', err);\n throw new Error('Failed to load recording processor');\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Create AudioWorklet node using Tone's method\n const workletNode = context.createAudioWorkletNode('recording-processor');\n workletNodeRef.current = workletNode;\n\n // Connect source to worklet (but not to destination - no monitoring)\n source.connect(workletNode);\n\n //Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { samples } = event.data;\n\n // Accumulate samples\n recordedChunksRef.current.push(samples);\n totalSamplesRef.current += samples.length;\n\n // Update peaks incrementally for live waveform visualization\n setPeaks((prevPeaks) =>\n appendPeaks(\n prevPeaks,\n samples,\n samplesPerPixel,\n totalSamplesRef.current - samples.length,\n bits\n )\n );\n\n // Note: VU meter levels come from useMicrophoneLevel (AnalyserNode)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Start the worklet processor\n workletNode.port.postMessage({\n command: 'start',\n sampleRate: context.sampleRate,\n channelCount,\n });\n\n // Reset state\n recordedChunksRef.current = [];\n totalSamplesRef.current = 0;\n setPeaks(new Int16Array(0));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n\n // Start duration update loop\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n } catch (err) {\n console.error('Failed to start recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, loadWorklet, isRecording, isPaused]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecording) {\n return null;\n }\n\n try {\n // Stop the worklet\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (e) {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated chunks\n const allSamples = concatenateAudioData(recordedChunksRef.current);\n const context = getContext();\n // Use rawContext for createBuffer (native AudioContext method)\n const rawContext = context.rawContext as AudioContext;\n const buffer = createAudioBuffer(\n rawContext,\n allSamples,\n rawContext.sampleRate,\n channelCount\n );\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.error('Failed to stop recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n }\n }, [isRecording, channelCount]);\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n }\n }, [isRecording, isPaused, duration]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (e) {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert Float32Array to AudioBuffer\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n samples: Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n const buffer = audioContext.createBuffer(\n channelCount,\n samples.length,\n sampleRate\n );\n\n // Copy samples to buffer (for now, just mono)\n // Create a new Float32Array to ensure correct type\n const typedSamples = new Float32Array(samples);\n buffer.copyToChannel(typedSamples, 0);\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, newSamples, sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, combined, sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n peakArray[i * 2] = Math.floor(min * maxValue);\n peakArray[i * 2 + 1] = Math.floor(max * maxValue);\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.floor(min * maxValue);\n updated[existingPeaks.length - 1] = Math.floor(max * maxValue);\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(\n err instanceof Error ? err : new Error('Failed to access microphone')\n );\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n }, [stream, enumerateDevices]);\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state and enumerate devices\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Cleanup on unmount\n return () => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, []);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses Tone.js Meter for real-time audio level monitoring.\n */\n\nimport { useEffect, useState, useRef } from 'react';\nimport { Meter, getContext, connect } from 'tone';\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * FFT size for the analyser\n * Default: 256\n */\n fftSize?: number;\n\n /**\n * Smoothing time constant (0-1)\n * Higher values = smoother but slower response\n * Default: 0.8\n */\n smoothingTimeConstant?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current audio level (0-1)\n * 0 = silence, 1 = maximum level\n */\n level: number;\n\n /**\n * Peak level since last reset (0-1)\n */\n peakLevel: number;\n\n /**\n * Reset the peak level\n */\n resetPeak: () => void;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current level and peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { level, peakLevel, resetPeak } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const {\n updateRate = 60,\n smoothingTimeConstant = 0.8,\n } = options;\n\n const [level, setLevel] = useState(0);\n const [peakLevel, setPeakLevel] = useState(0);\n\n const meterRef = useRef<Meter | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const animationFrameRef = useRef<number | null>(null);\n\n const resetPeak = () => setPeakLevel(0);\n\n useEffect(() => {\n if (!stream) {\n setLevel(0);\n setPeakLevel(0);\n return;\n }\n\n let isMounted = true;\n\n // Setup audio monitoring\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n // Get Tone's context and resume if needed\n const context = getContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n if (!isMounted) return;\n\n // Create Tone.js Meter for level monitoring\n // Pass context to ensure it's created in the same context as the source\n const meter = new Meter({ smoothing: smoothingTimeConstant, context });\n meterRef.current = meter;\n\n // Create MediaStreamSource from the SAME context as the meter\n // Note: This creates a separate source from useRecording, but that's OK\n // since we're only using it for level monitoring (not recording)\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n\n // Connect source to meter using Tone's connect function\n connect(source, meter);\n\n // Start level monitoring\n const updateInterval = 1000 / updateRate;\n let lastUpdateTime = 0;\n\n const updateLevel = (timestamp: number) => {\n if (!isMounted || !meterRef.current) return;\n\n if (timestamp - lastUpdateTime >= updateInterval) {\n lastUpdateTime = timestamp;\n\n // Meter.getValue() returns dB, convert to 0-1 range\n const db = meterRef.current.getValue();\n const dbValue = typeof db === 'number' ? db : db[0];\n // dB is typically -Infinity to 0, map -100dB..0dB to 0..1\n // Using -100dB as floor since Firefox seems to report lower values\n const normalized = Math.max(0, Math.min(1, (dbValue + 100) / 100));\n\n setLevel(normalized);\n setPeakLevel(prev => Math.max(prev, normalized));\n }\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n setupMonitoring();\n\n // Cleanup\n return () => {\n isMounted = false;\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Disconnect and clean up\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (e) {\n // Ignore disconnect errors\n }\n sourceRef.current = null;\n }\n\n if (meterRef.current) {\n meterRef.current.dispose();\n meterRef.current = null;\n }\n };\n }, [stream, smoothingTimeConstant, updateRate]);\n\n return {\n level,\n peakLevel,\n resetPeak,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip } from '@waveform-playlist/core';\nimport { resumeGlobalAudioContext } from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: Int8Array | Int16Array;\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Microphone access\n const {\n stream,\n devices,\n hasPermission,\n requestAccess,\n error: micError,\n } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const { level, peakLevel } = useMicrophoneLevel(stream);\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _audioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n const startRecording = useCallback(async () => {\n if (!selectedTrackId) {\n setHookError(new Error('Cannot start recording: no track selected. Select or create a track first.'));\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [selectedTrackId, isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n if (buffer && selectedTrackId) {\n const selectedTrackIndex = tracks.findIndex(t => t.id === selectedTrackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${selectedTrackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.error(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Calculate start position: max(currentTime, lastClipEndTime)\n const currentTimeSamples = Math.floor(currentTime * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n // Find the end time of the last clip (in samples)\n const endSamples = selectedTrack.clips.map(clip =>\n clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n // Use whichever is greater: cursor position or last clip end\n const startSample = Math.max(currentTimeSamples, lastClipEndSample);\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: buffer.length,\n offsetSamples: 0,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [selectedTrackId, tracks, setTracks, currentTime, stopRec]);\n\n // Auto-select the first device when devices become available\n useEffect(() => {\n // Only auto-select if we have permission, devices are available, and nothing is selected yet\n if (hasPermission && devices.length > 0 && selectedDevice === null) {\n setSelectedDevice(devices[0].deviceId);\n }\n }, [hasPermission, devices.length]); // Intentionally not including selectedDevice to avoid re-triggering\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n error: hookError || micError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n","/**\n * RecordButton - Control button for starting/stopping recording\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordButtonProps {\n isRecording: boolean;\n onClick: () => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Button = styled.button<{ $isRecording: boolean }>`\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border: none;\n border-radius: 0.25rem;\n cursor: pointer;\n transition: all 0.2s ease-in-out;\n background: ${(props) => (props.$isRecording ? '#dc3545' : '#e74c3c')};\n color: white;\n\n &:hover:not(:disabled) {\n background: ${(props) => (props.$isRecording ? '#c82333' : '#c0392b')};\n transform: translateY(-1px);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n &:focus {\n outline: none;\n box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.3);\n }\n`;\n\nconst RecordingIndicator = styled.span`\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: white;\n margin-right: 0.5rem;\n animation: pulse 1.5s ease-in-out infinite;\n\n @keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n`;\n\nexport const RecordButton: React.FC<RecordButtonProps> = ({\n isRecording,\n onClick,\n disabled = false,\n className,\n}) => {\n return (\n <Button\n $isRecording={isRecording}\n onClick={onClick}\n disabled={disabled}\n className={className}\n aria-label={isRecording ? 'Stop recording' : 'Start recording'}\n >\n {isRecording && <RecordingIndicator />}\n {isRecording ? 'Stop Recording' : 'Record'}\n </Button>\n );\n};\n","/**\n * MicrophoneSelector - Dropdown for selecting microphone input device\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\nimport { BaseSelect, BaseLabel } from '@waveform-playlist/ui-components';\nimport { MicrophoneDevice } from '../types';\n\nexport interface MicrophoneSelectorProps {\n devices: MicrophoneDevice[];\n selectedDeviceId?: string;\n onDeviceChange: (deviceId: string) => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Select = styled(BaseSelect)`\n min-width: 200px;\n`;\n\nconst Label = styled(BaseLabel)`\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n`;\n\nexport const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = ({\n devices,\n selectedDeviceId,\n onDeviceChange,\n disabled = false,\n className,\n}) => {\n const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {\n onDeviceChange(event.target.value);\n };\n\n // Use first device if no selection provided\n const currentValue = selectedDeviceId || (devices.length > 0 ? devices[0].deviceId : '');\n\n return (\n <Label className={className}>\n Microphone\n <Select\n value={currentValue}\n onChange={handleChange}\n disabled={disabled || devices.length === 0}\n >\n {devices.length === 0 ? (\n <option value=\"\">No microphones found</option>\n ) : (\n devices.map((device) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))\n )}\n </Select>\n </Label>\n );\n};\n","/**\n * RecordingIndicator - Shows recording status, duration, and visual indicator\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordingIndicatorProps {\n isRecording: boolean;\n isPaused?: boolean;\n duration: number; // in seconds\n formatTime?: (seconds: number) => string;\n className?: string;\n}\n\nconst Container = styled.div<{ $isRecording: boolean }>`\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.5rem 0.75rem;\n background: ${(props) => (props.$isRecording ? '#fff3cd' : 'transparent')};\n border-radius: 0.25rem;\n transition: background 0.2s ease-in-out;\n`;\n\nconst Dot = styled.div<{ $isRecording: boolean; $isPaused: boolean }>`\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n opacity: ${(props) => (props.$isRecording ? 1 : 0)};\n transition: opacity 0.2s ease-in-out;\n\n ${(props) =>\n props.$isRecording &&\n !props.$isPaused &&\n `\n animation: blink 1.5s ease-in-out infinite;\n\n @keyframes blink {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n `}\n`;\n\nconst Duration = styled.span`\n font-family: 'Courier New', Monaco, monospace;\n font-size: 1rem;\n font-weight: 600;\n color: #495057;\n min-width: 70px;\n`;\n\nconst Status = styled.span<{ $isPaused: boolean }>`\n font-size: 0.75rem;\n font-weight: 500;\n color: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n text-transform: uppercase;\n`;\n\nconst defaultFormatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n};\n\nexport const RecordingIndicator: React.FC<RecordingIndicatorProps> = ({\n isRecording,\n isPaused = false,\n duration,\n formatTime = defaultFormatTime,\n className,\n}) => {\n return (\n <Container $isRecording={isRecording} className={className}>\n <Dot $isRecording={isRecording} $isPaused={isPaused} />\n <Duration>{formatTime(duration)}</Duration>\n {isRecording && <Status $isPaused={isPaused}>{isPaused ? 'Paused' : 'Recording'}</Status>}\n </Container>\n );\n};\n","/**\n * VU Meter Component\n *\n * Displays real-time audio input levels with color-coded zones\n * and peak indicator.\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface VUMeterProps {\n /**\n * Current audio level (0-1)\n */\n level: number;\n\n /**\n * Peak level (0-1)\n * Optional - if provided, shows peak indicator\n */\n peakLevel?: number;\n\n /**\n * Width of the meter in pixels\n * Default: 200\n */\n width?: number;\n\n /**\n * Height of the meter in pixels\n * Default: 20\n */\n height?: number;\n\n /**\n * Additional CSS class name\n */\n className?: string;\n}\n\nconst MeterContainer = styled.div<{ $width: number; $height: number }>`\n position: relative;\n width: ${props => props.$width}px;\n height: ${props => props.$height}px;\n background: #2c3e50;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);\n`;\n\n// Helper to get gradient color based on level\nconst getLevelGradient = (level: number): string => {\n if (level < 0.6) return 'linear-gradient(90deg, #27ae60, #2ecc71)';\n if (level < 0.85) return 'linear-gradient(90deg, #f39c12, #f1c40f)';\n return 'linear-gradient(90deg, #c0392b, #e74c3c)';\n};\n\n// Use .attrs() for frequently changing styles to avoid generating new CSS classes\nconst MeterFill = styled.div.attrs<{ $level: number; $height: number }>(props => ({\n style: {\n width: `${props.$level * 100}%`,\n height: `${props.$height}px`,\n background: getLevelGradient(props.$level),\n boxShadow: props.$level > 0.01 ? '0 0 8px rgba(255, 255, 255, 0.3)' : 'none',\n },\n}))<{ $level: number; $height: number }>`\n position: absolute;\n left: 0;\n top: 0;\n transition: width 0.05s ease-out, background 0.1s ease-out;\n`;\n\n// Use .attrs() for frequently changing left position\nconst PeakIndicator = styled.div.attrs<{ $peakLevel: number; $height: number }>(props => ({\n style: {\n left: `${props.$peakLevel * 100}%`,\n height: `${props.$height}px`,\n },\n}))<{ $peakLevel: number; $height: number }>`\n position: absolute;\n top: 0;\n width: 2px;\n background: #ecf0f1;\n box-shadow: 0 0 4px rgba(236, 240, 241, 0.8);\n transition: left 0.1s ease-out;\n`;\n\nconst ScaleMarkers = styled.div<{ $height: number }>`\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: ${props => props.$height}px;\n pointer-events: none;\n`;\n\nconst ScaleMark = styled.div<{ $position: number; $height: number }>`\n position: absolute;\n left: ${props => props.$position}%;\n top: 0;\n width: 1px;\n height: ${props => props.$height}px;\n background: rgba(255, 255, 255, 0.2);\n`;\n\n/**\n * VU Meter component for displaying audio input levels\n *\n * @example\n * ```typescript\n * import { useMicrophoneLevel, VUMeter } from '@waveform-playlist/recording';\n *\n * const { level, peakLevel } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} width={300} height={24} />;\n * ```\n */\nconst VUMeterComponent: React.FC<VUMeterProps> = ({\n level,\n peakLevel,\n width = 200,\n height = 20,\n className,\n}) => {\n // Clamp values to 0-1 range\n const clampedLevel = Math.max(0, Math.min(1, level));\n const clampedPeak = peakLevel !== undefined\n ? Math.max(0, Math.min(1, peakLevel))\n : 0;\n\n return (\n <MeterContainer $width={width} $height={height} className={className}>\n <MeterFill $level={clampedLevel} $height={height} />\n\n {peakLevel !== undefined && clampedPeak > 0 && (\n <PeakIndicator $peakLevel={clampedPeak} $height={height} />\n )}\n\n <ScaleMarkers $height={height}>\n <ScaleMark $position={60} $height={height} />\n <ScaleMark $position={85} $height={height} />\n </ScaleMarkers>\n </MeterContainer>\n );\n};\n\n// Memoize to prevent unnecessary re-renders when parent updates\nexport const VUMeter = React.memo(VUMeterComponent);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,mBAAyD;;;ACGlD,SAAS,qBAAqB,QAAsC;AACzE,QAAM,cAAc,OAAO,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,QAAQ,CAAC;AACvE,QAAM,SAAS,IAAI,aAAa,WAAW;AAE3C,MAAI,SAAS;AACb,aAAW,SAAS,QAAQ;AAC1B,WAAO,IAAI,OAAO,MAAM;AACxB,cAAU,MAAM;AAAA,EAClB;AAEA,SAAO;AACT;AAKO,SAAS,kBACd,cACA,SACA,YACA,eAAuB,GACV;AACb,QAAM,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,EACF;AAIA,QAAM,eAAe,IAAI,aAAa,OAAO;AAC7C,SAAO,cAAc,cAAc,CAAC;AAEpC,SAAO;AACT;;;AC5BO,SAAS,cACd,SACA,iBACA,OAAe,IACS;AACxB,QAAM,WAAW,KAAK,KAAK,QAAQ,SAAS,eAAe;AAC3D,QAAM,YAAY,SAAS,IAAI,IAAI,UAAU,WAAW,CAAC,IAAI,IAAI,WAAW,WAAW,CAAC;AACxF,QAAM,WAAW,MAAM,OAAO;AAE9B,WAAS,IAAI,GAAG,IAAI,UAAU,KAAK;AACjC,UAAM,QAAQ,IAAI;AAClB,UAAM,MAAM,KAAK,IAAI,QAAQ,iBAAiB,QAAQ,MAAM;AAE5D,QAAI,MAAM;AACV,QAAI,MAAM;AAEV,aAAS,IAAI,OAAO,IAAI,KAAK,KAAK;AAChC,YAAM,QAAQ,QAAQ,CAAC;AACvB,UAAI,QAAQ,IAAK,OAAM;AACvB,UAAI,QAAQ,IAAK,OAAM;AAAA,IACzB;AAGA,cAAU,IAAI,CAAC,IAAI,KAAK,MAAM,MAAM,QAAQ;AAC5C,cAAU,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,MAAM,QAAQ;AAAA,EAClD;AAEA,SAAO;AACT;AAMO,SAAS,YACd,eACA,YACA,iBACA,uBACA,OAAe,IACS;AACxB,QAAM,WAAW,MAAM,OAAO;AAG9B,QAAM,YAAY,wBAAwB;AAC1C,MAAI,SAAS;AAGb,MAAI,YAAY,KAAK,cAAc,SAAS,GAAG;AAC7C,UAAM,oBAAoB,kBAAkB;AAC5C,UAAM,WAAW,KAAK,IAAI,mBAAmB,WAAW,MAAM;AAG9D,QAAI,MAAM,cAAc,cAAc,SAAS,CAAC,IAAI;AACpD,QAAI,MAAM,cAAc,cAAc,SAAS,CAAC,IAAI;AAGpD,aAAS,IAAI,GAAG,IAAI,UAAU,KAAK;AACjC,YAAM,QAAQ,WAAW,CAAC;AAC1B,UAAI,QAAQ,IAAK,OAAM;AACvB,UAAI,QAAQ,IAAK,OAAM;AAAA,IACzB;AAGA,UAAM,UAAU,KAAK,SAAS,IAAI,YAAY,YAAY,cAAc,MAAM;AAC9E,YAAQ,IAAI,aAAa;AACzB,YAAQ,cAAc,SAAS,CAAC,IAAI,KAAK,MAAM,MAAM,QAAQ;AAC7D,YAAQ,cAAc,SAAS,CAAC,IAAI,KAAK,MAAM,MAAM,QAAQ;AAE7D,aAAS;AAGT,UAAMC,YAAW,cAAc,WAAW,MAAM,MAAM,GAAG,iBAAiB,IAAI;AAC9E,UAAMC,UAAS,KAAK,SAAS,IAAI,YAAY,YAAY,QAAQ,SAASD,UAAS,MAAM;AACzF,IAAAC,QAAO,IAAI,OAAO;AAClB,IAAAA,QAAO,IAAID,WAAU,QAAQ,MAAM;AACnC,WAAOC;AAAA,EACT;AAGA,QAAM,WAAW,cAAc,WAAW,MAAM,MAAM,GAAG,iBAAiB,IAAI;AAC9E,QAAM,SAAS,KAAK,SAAS,IAAI,YAAY,YAAY,cAAc,SAAS,SAAS,MAAM;AAC/F,SAAO,IAAI,aAAa;AACxB,SAAO,IAAI,UAAU,cAAc,MAAM;AACzC,SAAO;AACT;;;AF1FA,kBAA2B;AAR3B;AAUO,SAAS,aACd,QACA,UAA4B,CAAC,GACT;AACpB,QAAM;AAAA,IACJ,eAAe;AAAA,IACf,kBAAkB;AAAA,EACpB,IAAI;AAGJ,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAS,KAAK;AACpD,QAAM,CAAC,UAAU,WAAW,QAAI,uBAAS,KAAK;AAC9C,QAAM,CAAC,UAAU,WAAW,QAAI,uBAAS,CAAC;AAC1C,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAiC,IAAI,WAAW,CAAC,CAAC;AAC5E,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;AAE5C,QAAM,OAAe;AAIrB,QAAM,uBAAmB,qBAAgB,KAAK;AAG9C,QAAM,qBAAiB,qBAAgC,IAAI;AAC3D,QAAM,2BAAuB,qBAA0C,IAAI;AAC3E,QAAM,wBAAoB,qBAAuB,CAAC,CAAC;AACnD,QAAM,sBAAkB,qBAAO,CAAC;AAChC,QAAM,wBAAoB,qBAAsB,IAAI;AACpD,QAAM,mBAAe,qBAAe,CAAC;AACrC,QAAM,qBAAiB,qBAAgB,KAAK;AAC5C,QAAM,kBAAc,qBAAgB,KAAK;AAGzC,QAAM,kBAAc,0BAAY,YAAY;AAE1C,QAAI,iBAAiB,SAAS;AAC5B;AAAA,IACF;AAEA,QAAI;AACF,YAAM,cAAU,wBAAW;AAG3B,YAAM,aAAa,IAAI;AAAA,QACrB;AAAA,QACA,YAAY;AAAA,MACd,EAAE;AAGF,YAAM,QAAQ,sBAAsB,UAAU;AAC9C,uBAAiB,UAAU;AAAA,IAC7B,SAAS,KAAK;AACZ,cAAQ,MAAM,uCAAuC,GAAG;AACxD,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,qBAAiB,0BAAY,YAAY;AAC7C,QAAI,CAAC,QAAQ;AACX,eAAS,IAAI,MAAM,gCAAgC,CAAC;AACpD;AAAA,IACF;AAEA,QAAI;AACF,eAAS,IAAI;AAGb,YAAM,cAAU,wBAAW;AAG3B,UAAI,QAAQ,UAAU,aAAa;AACjC,cAAM,QAAQ,OAAO;AAAA,MACvB;AAGA,YAAM,YAAY;AAIlB,YAAM,SAAS,QAAQ,wBAAwB,MAAM;AACrD,2BAAqB,UAAU;AAG/B,YAAM,cAAc,QAAQ,uBAAuB,qBAAqB;AACxE,qBAAe,UAAU;AAGzB,aAAO,QAAQ,WAAW;AAG1B,kBAAY,KAAK,YAAY,CAAC,UAAwB;AACpD,cAAM,EAAE,QAAQ,IAAI,MAAM;AAG1B,0BAAkB,QAAQ,KAAK,OAAO;AACtC,wBAAgB,WAAW,QAAQ;AAGnC;AAAA,UAAS,CAAC,cACR;AAAA,YACE;AAAA,YACA;AAAA,YACA;AAAA,YACA,gBAAgB,UAAU,QAAQ;AAAA,YAClC;AAAA,UACF;AAAA,QACF;AAAA,MAIF;AAGA,kBAAY,KAAK,YAAY;AAAA,QAC3B,SAAS;AAAA,QACT,YAAY,QAAQ;AAAA,QACpB;AAAA,MACF,CAAC;AAGD,wBAAkB,UAAU,CAAC;AAC7B,sBAAgB,UAAU;AAC1B,eAAS,IAAI,WAAW,CAAC,CAAC;AAC1B,qBAAe,IAAI;AACnB,eAAS,CAAC;AACV,mBAAa,CAAC;AACd,qBAAe,UAAU;AACzB,kBAAY,UAAU;AACtB,qBAAe,IAAI;AACnB,kBAAY,KAAK;AACjB,mBAAa,UAAU,YAAY,IAAI;AAGvC,YAAM,iBAAiB,MAAM;AAC3B,YAAI,eAAe,WAAW,CAAC,YAAY,SAAS;AAClD,gBAAM,WAAW,YAAY,IAAI,IAAI,aAAa,WAAW;AAC7D,sBAAY,OAAO;AACnB,4BAAkB,UAAU,sBAAsB,cAAc;AAAA,QAClE;AAAA,MACF;AACA,qBAAe;AAAA,IACjB,SAAS,KAAK;AACZ,cAAQ,MAAM,8BAA8B,GAAG;AAC/C,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,2BAA2B,CAAC;AAAA,IAC9E;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,iBAAiB,aAAa,aAAa,QAAQ,CAAC;AAG9E,QAAM,oBAAgB,0BAAY,YAAyC;AACzE,QAAI,CAAC,aAAa;AAChB,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,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,GAAG;AAAA,UAGZ;AAAA,QACF;AACA,uBAAe,QAAQ,WAAW;AAAA,MACpC;AAGA,UAAI,kBAAkB,YAAY,MAAM;AACtC,6BAAqB,kBAAkB,OAAO;AAC9C,0BAAkB,UAAU;AAAA,MAC9B;AAGA,YAAM,aAAa,qBAAqB,kBAAkB,OAAO;AACjE,YAAM,cAAU,wBAAW;AAE3B,YAAM,aAAa,QAAQ;AAC3B,YAAM,SAAS;AAAA,QACb;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX;AAAA,MACF;AAEA,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,MAAM,6BAA6B,GAAG;AAC9C,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,0BAA0B,CAAC;AAC3E,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,aAAa,YAAY,CAAC;AAG9B,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;AAEtD,YAAM,iBAAiB,MAAM;AAC3B,YAAI,eAAe,WAAW,CAAC,YAAY,SAAS;AAClD,gBAAM,WAAW,YAAY,IAAI,IAAI,aAAa,WAAW;AAC7D,sBAAY,OAAO;AACnB,4BAAkB,UAAU,sBAAsB,cAAc;AAAA,QAClE;AAAA,MACF;AACA,qBAAe;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,aAAa,UAAU,QAAQ,CAAC;AAGpC,8BAAU,MAAM;AACd,WAAO,MAAM;AACX,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,GAAG;AAAA,UAGZ;AAAA,QACF;AACA,uBAAe,QAAQ,WAAW;AAAA,MACpC;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;;;AG3RA,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,2BAAY,OAAO,UAAmB,qBAA6C;AACvG,iBAAa,IAAI;AACjB,aAAS,IAAI;AAEb,QAAI;AAEF,UAAI,QAAQ;AACV,eAAO,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAAA,MACpD;AAGA,YAAM,QAAsD;AAAA;AAAA,QAE1D,kBAAkB;AAAA,QAClB,kBAAkB;AAAA,QAClB,iBAAiB;AAAA,QACjB,SAAS;AAAA;AAAA;AAAA,QAET,GAAG;AAAA;AAAA,QAEH,GAAI,YAAY,EAAE,UAAU,EAAE,OAAO,SAAS,EAAE;AAAA,MAClD;AAEA,YAAM,cAAsC;AAAA,QAC1C;AAAA,QACA,OAAO;AAAA,MACT;AAEA,YAAM,YAAY,MAAM,UAAU,aAAa,aAAa,WAAW;AACvE,gBAAU,SAAS;AACnB,uBAAiB,IAAI;AAGrB,YAAM,iBAAiB;AAAA,IACzB,SAAS,KAAK;AACZ,cAAQ,MAAM,gCAAgC,GAAG;AACjD;AAAA,QACE,eAAe,QAAQ,MAAM,IAAI,MAAM,6BAA6B;AAAA,MACtE;AACA,uBAAiB,KAAK;AAAA,IACxB,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,QAAQ,gBAAgB,CAAC;AAG7B,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,WAAO,MAAM;AACX,UAAI,QAAQ;AACV,eAAO,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;ACxGA,IAAAC,gBAA4C;AAC5C,IAAAC,eAA2C;AAwDpC,SAAS,mBACd,QACA,UAAqC,CAAC,GACZ;AAC1B,QAAM;AAAA,IACJ,aAAa;AAAA,IACb,wBAAwB;AAAA,EAC1B,IAAI;AAEJ,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAS,CAAC;AACpC,QAAM,CAAC,WAAW,YAAY,QAAI,wBAAS,CAAC;AAE5C,QAAM,eAAW,sBAAqB,IAAI;AAC1C,QAAM,gBAAY,sBAA0C,IAAI;AAChE,QAAM,wBAAoB,sBAAsB,IAAI;AAEpD,QAAM,YAAY,MAAM,aAAa,CAAC;AAEtC,+BAAU,MAAM;AACd,QAAI,CAAC,QAAQ;AACX,eAAS,CAAC;AACV,mBAAa,CAAC;AACd;AAAA,IACF;AAEA,QAAI,YAAY;AAGhB,UAAM,kBAAkB,YAAY;AAClC,UAAI,CAAC,UAAW;AAGhB,YAAM,cAAU,yBAAW;AAC3B,UAAI,QAAQ,UAAU,aAAa;AACjC,cAAM,QAAQ,OAAO;AAAA,MACvB;AAEA,UAAI,CAAC,UAAW;AAIhB,YAAM,QAAQ,IAAI,mBAAM,EAAE,WAAW,uBAAuB,QAAQ,CAAC;AACrE,eAAS,UAAU;AAKnB,YAAM,SAAS,QAAQ,wBAAwB,MAAM;AACrD,gBAAU,UAAU;AAGpB,gCAAQ,QAAQ,KAAK;AAGrB,YAAM,iBAAiB,MAAO;AAC9B,UAAI,iBAAiB;AAErB,YAAM,cAAc,CAAC,cAAsB;AACzC,YAAI,CAAC,aAAa,CAAC,SAAS,QAAS;AAErC,YAAI,YAAY,kBAAkB,gBAAgB;AAChD,2BAAiB;AAGjB,gBAAM,KAAK,SAAS,QAAQ,SAAS;AACrC,gBAAM,UAAU,OAAO,OAAO,WAAW,KAAK,GAAG,CAAC;AAGlD,gBAAM,aAAa,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,UAAU,OAAO,GAAG,CAAC;AAEjE,mBAAS,UAAU;AACnB,uBAAa,UAAQ,KAAK,IAAI,MAAM,UAAU,CAAC;AAAA,QACjD;AAEA,0BAAkB,UAAU,sBAAsB,WAAW;AAAA,MAC/D;AAEA,wBAAkB,UAAU,sBAAsB,WAAW;AAAA,IAC/D;AAEA,oBAAgB;AAGhB,WAAO,MAAM;AACX,kBAAY;AAEZ,UAAI,kBAAkB,SAAS;AAC7B,6BAAqB,kBAAkB,OAAO;AAC9C,0BAAkB,UAAU;AAAA,MAC9B;AAGA,UAAI,UAAU,SAAS;AACrB,YAAI;AACF,oBAAU,QAAQ,WAAW;AAAA,QAC/B,SAAS,GAAG;AAAA,QAEZ;AACA,kBAAU,UAAU;AAAA,MACtB;AAEA,UAAI,SAAS,SAAS;AACpB,iBAAS,QAAQ,QAAQ;AACzB,iBAAS,UAAU;AAAA,MACrB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,uBAAuB,UAAU,CAAC;AAE9C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC3KA,IAAAC,gBAAiD;AAMjD,qBAAyC;AAuDlC,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;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT,IAAI,oBAAoB;AAGxB,QAAM,EAAE,OAAO,UAAU,IAAI,mBAAmB,MAAM;AAGtD,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;AAGzC,QAAM,qBAAiB,2BAAY,YAAY;AAC7C,QAAI,CAAC,iBAAiB;AACpB,mBAAa,IAAI,MAAM,4EAA4E,CAAC;AACpG;AAAA,IACF;AAEA,QAAI;AACF,mBAAa,IAAI;AAEjB,UAAI,CAAC,cAAc;AACjB,kBAAM,yCAAyB;AAC/B,wBAAgB,IAAI;AAAA,MACtB;AAEA,YAAM,SAAS;AAAA,IACjB,SAAS,KAAK;AACZ,mBAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAClE;AAAA,EACF,GAAG,CAAC,iBAAiB,cAAc,QAAQ,CAAC;AAG5C,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,QAAI,UAAU,iBAAiB;AAC7B,YAAM,qBAAqB,OAAO,UAAU,OAAK,EAAE,OAAO,eAAe;AACzE,UAAI,uBAAuB,IAAI;AAC7B,cAAM,MAAM,IAAI;AAAA,UACd,kCAAkC,eAAe;AAAA,QACnD;AACA,gBAAQ,MAAM,uBAAuB,IAAI,OAAO,EAAE;AAClD,qBAAa,GAAG;AAChB;AAAA,MACF;AAEA,YAAM,gBAAgB,OAAO,kBAAkB;AAG/C,YAAM,qBAAqB,KAAK,MAAM,cAAc,OAAO,UAAU;AAErE,UAAI,oBAAoB;AACxB,UAAI,cAAc,MAAM,SAAS,GAAG;AAElC,cAAM,aAAa,cAAc,MAAM;AAAA,UAAI,UACzC,KAAK,cAAc,KAAK;AAAA,QAC1B;AACA,4BAAoB,KAAK,IAAI,GAAG,UAAU;AAAA,MAC5C;AAGA,YAAM,cAAc,KAAK,IAAI,oBAAoB,iBAAiB;AAGlE,YAAM,UAAqB;AAAA,QACzB,IAAI,QAAQ,KAAK,IAAI,CAAC;AAAA,QACtB,aAAa;AAAA,QACb;AAAA,QACA,iBAAiB,OAAO;AAAA,QACxB,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,iBAAiB,QAAQ,WAAW,aAAa,OAAO,CAAC;AAG7D,+BAAU,MAAM;AAEd,QAAI,iBAAiB,QAAQ,SAAS,KAAK,mBAAmB,MAAM;AAClE,wBAAkB,QAAQ,CAAC,EAAE,QAAQ;AAAA,IACvC;AAAA,EACF,GAAG,CAAC,eAAe,QAAQ,MAAM,CAAC;AAGlC,QAAM,uBAAmB,2BAAY,YAAY;AAC/C,QAAI;AACF,mBAAa,IAAI;AACjB,YAAM,cAAc,QAAW,gBAAgB;AAC/C,gBAAM,yCAAyB;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,2BAAY,OAAO,aAAqB;AAC3D,QAAI;AACF,mBAAa,IAAI;AACjB,wBAAkB,QAAQ;AAC1B,YAAM,cAAc,UAAU,gBAAgB;AAC9C,gBAAM,yCAAyB;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;AAEpC,SAAO;AAAA;AAAA,IAEL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,aAAa,YAAY;AAAA;AAAA,IAGhC;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;;;ACvPA,+BAAmB;AAoEf;AA3DJ,IAAM,SAAS,yBAAAC,QAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAQN,CAAC,UAAW,MAAM,eAAe,YAAY,SAAU;AAAA;AAAA;AAAA;AAAA,kBAIrD,CAAC,UAAW,MAAM,eAAe,YAAY,SAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBzE,IAAM,qBAAqB,yBAAAA,QAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoB3B,IAAM,eAA4C,CAAC;AAAA,EACxD;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,MAAM;AACJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAc;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,cAAY,cAAc,mBAAmB;AAAA,MAE5C;AAAA,uBAAe,4CAAC,sBAAmB;AAAA,QACnC,cAAc,mBAAmB;AAAA;AAAA;AAAA,EACpC;AAEJ;;;AC/EA,IAAAC,4BAAmB;AACnB,2BAAsC;AAoClC,IAAAC,sBAAA;AAzBJ,IAAM,aAAS,0BAAAC,SAAO,+BAAU;AAAA;AAAA;AAIhC,IAAM,YAAQ,0BAAAA,SAAO,8BAAS;AAAA;AAAA;AAAA;AAAA;AAMvB,IAAM,qBAAwD,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,MAAM;AACJ,QAAM,eAAe,CAAC,UAAgD;AACpE,mBAAe,MAAM,OAAO,KAAK;AAAA,EACnC;AAGA,QAAM,eAAe,qBAAqB,QAAQ,SAAS,IAAI,QAAQ,CAAC,EAAE,WAAW;AAErF,SACE,8CAAC,SAAM,WAAsB;AAAA;AAAA,IAE3B;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,UAAU;AAAA,QACV,UAAU,YAAY,QAAQ,WAAW;AAAA,QAExC,kBAAQ,WAAW,IAClB,6CAAC,YAAO,OAAM,IAAG,kCAAoB,IAErC,QAAQ,IAAI,CAAC,WACX,6CAAC,YAA6B,OAAO,OAAO,UACzC,iBAAO,SADG,OAAO,QAEpB,CACD;AAAA;AAAA,IAEL;AAAA,KACF;AAEJ;;;ACxDA,IAAAC,4BAAmB;AA0Ef,IAAAC,sBAAA;AAhEJ,IAAM,YAAY,0BAAAC,QAAO;AAAA;AAAA;AAAA;AAAA;AAAA,gBAKT,CAAC,UAAW,MAAM,eAAe,YAAY,aAAc;AAAA;AAAA;AAAA;AAK3E,IAAM,MAAM,0BAAAA,QAAO;AAAA;AAAA;AAAA;AAAA,gBAIH,CAAC,UAAW,MAAM,YAAY,YAAY,SAAU;AAAA,aACvD,CAAC,UAAW,MAAM,eAAe,IAAI,CAAE;AAAA;AAAA;AAAA,IAGhD,CAAC,UACD,MAAM,gBACN,CAAC,MAAM,aACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAWD;AAAA;AAGH,IAAM,WAAW,0BAAAA,QAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQxB,IAAM,SAAS,0BAAAA,QAAO;AAAA;AAAA;AAAA,WAGX,CAAC,UAAW,MAAM,YAAY,YAAY,SAAU;AAAA;AAAA;AAI/D,IAAM,oBAAoB,CAAC,YAA4B;AACrD,QAAM,OAAO,KAAK,MAAM,UAAU,EAAE;AACpC,QAAM,OAAO,KAAK,MAAM,UAAU,EAAE;AACpC,SAAO,GAAG,KAAK,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAChF;AAEO,IAAMC,sBAAwD,CAAC;AAAA,EACpE;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,aAAa;AAAA,EACb;AACF,MAAM;AACJ,SACE,8CAAC,aAAU,cAAc,aAAa,WACpC;AAAA,iDAAC,OAAI,cAAc,aAAa,WAAW,UAAU;AAAA,IACrD,6CAAC,YAAU,qBAAW,QAAQ,GAAE;AAAA,IAC/B,eAAe,6CAAC,UAAO,WAAW,UAAW,qBAAW,WAAW,aAAY;AAAA,KAClF;AAEJ;;;AC9EA,IAAAC,gBAAkB;AAClB,IAAAC,4BAAmB;AA4Hb,IAAAC,sBAAA;AA5FN,IAAM,iBAAiB,0BAAAC,QAAO;AAAA;AAAA,WAEnB,WAAS,MAAM,MAAM;AAAA,YACpB,WAAS,MAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAQlC,IAAM,mBAAmB,CAAC,UAA0B;AAClD,MAAI,QAAQ,IAAK,QAAO;AACxB,MAAI,QAAQ,KAAM,QAAO;AACzB,SAAO;AACT;AAGA,IAAM,YAAY,0BAAAA,QAAO,IAAI,MAA2C,YAAU;AAAA,EAChF,OAAO;AAAA,IACL,OAAO,GAAG,MAAM,SAAS,GAAG;AAAA,IAC5B,QAAQ,GAAG,MAAM,OAAO;AAAA,IACxB,YAAY,iBAAiB,MAAM,MAAM;AAAA,IACzC,WAAW,MAAM,SAAS,OAAO,qCAAqC;AAAA,EACxE;AACF,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAQF,IAAM,gBAAgB,0BAAAA,QAAO,IAAI,MAA+C,YAAU;AAAA,EACxF,OAAO;AAAA,IACL,MAAM,GAAG,MAAM,aAAa,GAAG;AAAA,IAC/B,QAAQ,GAAG,MAAM,OAAO;AAAA,EAC1B;AACF,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASF,IAAM,eAAe,0BAAAA,QAAO;AAAA;AAAA;AAAA;AAAA;AAAA,YAKhB,WAAS,MAAM,OAAO;AAAA;AAAA;AAIlC,IAAM,YAAY,0BAAAA,QAAO;AAAA;AAAA,UAEf,WAAS,MAAM,SAAS;AAAA;AAAA;AAAA,YAGtB,WAAS,MAAM,OAAO;AAAA;AAAA;AAgBlC,IAAM,mBAA2C,CAAC;AAAA,EAChD;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,SAAS;AAAA,EACT;AACF,MAAM;AAEJ,QAAM,eAAe,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACnD,QAAM,cAAc,cAAc,SAC9B,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC,IAClC;AAEJ,SACE,8CAAC,kBAAe,QAAQ,OAAO,SAAS,QAAQ,WAC9C;AAAA,iDAAC,aAAU,QAAQ,cAAc,SAAS,QAAQ;AAAA,IAEjD,cAAc,UAAa,cAAc,KACxC,6CAAC,iBAAc,YAAY,aAAa,SAAS,QAAQ;AAAA,IAG3D,8CAAC,gBAAa,SAAS,QACrB;AAAA,mDAAC,aAAU,WAAW,IAAI,SAAS,QAAQ;AAAA,MAC3C,6CAAC,aAAU,WAAW,IAAI,SAAS,QAAQ;AAAA,OAC7C;AAAA,KACF;AAEJ;AAGO,IAAM,UAAU,cAAAC,QAAM,KAAK,gBAAgB;","names":["RecordingIndicator","newPeaks","result","import_react","import_react","import_tone","import_react","styled","import_styled_components","import_jsx_runtime","styled","import_styled_components","import_jsx_runtime","styled","RecordingIndicator","import_react","import_styled_components","import_jsx_runtime","styled","React"]}
|
package/dist/index.mjs
CHANGED
|
@@ -438,6 +438,152 @@ function useMicrophoneLevel(stream, options = {}) {
|
|
|
438
438
|
};
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
// src/hooks/useIntegratedRecording.ts
|
|
442
|
+
import { useState as useState4, useCallback as useCallback3, useEffect as useEffect4 } from "react";
|
|
443
|
+
import { resumeGlobalAudioContext } from "@waveform-playlist/playout";
|
|
444
|
+
function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}) {
|
|
445
|
+
const { currentTime = 0, audioConstraints, ...recordingOptions } = options;
|
|
446
|
+
const [isMonitoring, setIsMonitoring] = useState4(false);
|
|
447
|
+
const [selectedDevice, setSelectedDevice] = useState4(null);
|
|
448
|
+
const [hookError, setHookError] = useState4(null);
|
|
449
|
+
const {
|
|
450
|
+
stream,
|
|
451
|
+
devices,
|
|
452
|
+
hasPermission,
|
|
453
|
+
requestAccess,
|
|
454
|
+
error: micError
|
|
455
|
+
} = useMicrophoneAccess();
|
|
456
|
+
const { level, peakLevel } = useMicrophoneLevel(stream);
|
|
457
|
+
const {
|
|
458
|
+
isRecording,
|
|
459
|
+
isPaused,
|
|
460
|
+
duration,
|
|
461
|
+
peaks,
|
|
462
|
+
audioBuffer: _audioBuffer,
|
|
463
|
+
startRecording: startRec,
|
|
464
|
+
stopRecording: stopRec,
|
|
465
|
+
pauseRecording,
|
|
466
|
+
resumeRecording,
|
|
467
|
+
error: recError
|
|
468
|
+
} = useRecording(stream, recordingOptions);
|
|
469
|
+
const startRecording = useCallback3(async () => {
|
|
470
|
+
if (!selectedTrackId) {
|
|
471
|
+
setHookError(new Error("Cannot start recording: no track selected. Select or create a track first."));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
setHookError(null);
|
|
476
|
+
if (!isMonitoring) {
|
|
477
|
+
await resumeGlobalAudioContext();
|
|
478
|
+
setIsMonitoring(true);
|
|
479
|
+
}
|
|
480
|
+
await startRec();
|
|
481
|
+
} catch (err) {
|
|
482
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
483
|
+
}
|
|
484
|
+
}, [selectedTrackId, isMonitoring, startRec]);
|
|
485
|
+
const stopRecording = useCallback3(async () => {
|
|
486
|
+
let buffer;
|
|
487
|
+
try {
|
|
488
|
+
buffer = await stopRec();
|
|
489
|
+
} catch (err) {
|
|
490
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (buffer && selectedTrackId) {
|
|
494
|
+
const selectedTrackIndex = tracks.findIndex((t) => t.id === selectedTrackId);
|
|
495
|
+
if (selectedTrackIndex === -1) {
|
|
496
|
+
const err = new Error(
|
|
497
|
+
`Recording completed but track "${selectedTrackId}" no longer exists. The recorded audio could not be saved.`
|
|
498
|
+
);
|
|
499
|
+
console.error(`[waveform-playlist] ${err.message}`);
|
|
500
|
+
setHookError(err);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const selectedTrack = tracks[selectedTrackIndex];
|
|
504
|
+
const currentTimeSamples = Math.floor(currentTime * buffer.sampleRate);
|
|
505
|
+
let lastClipEndSample = 0;
|
|
506
|
+
if (selectedTrack.clips.length > 0) {
|
|
507
|
+
const endSamples = selectedTrack.clips.map(
|
|
508
|
+
(clip) => clip.startSample + clip.durationSamples
|
|
509
|
+
);
|
|
510
|
+
lastClipEndSample = Math.max(...endSamples);
|
|
511
|
+
}
|
|
512
|
+
const startSample = Math.max(currentTimeSamples, lastClipEndSample);
|
|
513
|
+
const newClip = {
|
|
514
|
+
id: `clip-${Date.now()}`,
|
|
515
|
+
audioBuffer: buffer,
|
|
516
|
+
startSample,
|
|
517
|
+
durationSamples: buffer.length,
|
|
518
|
+
offsetSamples: 0,
|
|
519
|
+
sampleRate: buffer.sampleRate,
|
|
520
|
+
sourceDurationSamples: buffer.length,
|
|
521
|
+
gain: 1,
|
|
522
|
+
name: `Recording ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`
|
|
523
|
+
};
|
|
524
|
+
const newTracks = tracks.map((track, index) => {
|
|
525
|
+
if (index === selectedTrackIndex) {
|
|
526
|
+
return {
|
|
527
|
+
...track,
|
|
528
|
+
clips: [...track.clips, newClip]
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return track;
|
|
532
|
+
});
|
|
533
|
+
setTracks(newTracks);
|
|
534
|
+
}
|
|
535
|
+
}, [selectedTrackId, tracks, setTracks, currentTime, stopRec]);
|
|
536
|
+
useEffect4(() => {
|
|
537
|
+
if (hasPermission && devices.length > 0 && selectedDevice === null) {
|
|
538
|
+
setSelectedDevice(devices[0].deviceId);
|
|
539
|
+
}
|
|
540
|
+
}, [hasPermission, devices.length]);
|
|
541
|
+
const requestMicAccess = useCallback3(async () => {
|
|
542
|
+
try {
|
|
543
|
+
setHookError(null);
|
|
544
|
+
await requestAccess(void 0, audioConstraints);
|
|
545
|
+
await resumeGlobalAudioContext();
|
|
546
|
+
setIsMonitoring(true);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
549
|
+
}
|
|
550
|
+
}, [requestAccess, audioConstraints]);
|
|
551
|
+
const changeDevice = useCallback3(async (deviceId) => {
|
|
552
|
+
try {
|
|
553
|
+
setHookError(null);
|
|
554
|
+
setSelectedDevice(deviceId);
|
|
555
|
+
await requestAccess(deviceId, audioConstraints);
|
|
556
|
+
await resumeGlobalAudioContext();
|
|
557
|
+
setIsMonitoring(true);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
560
|
+
}
|
|
561
|
+
}, [requestAccess, audioConstraints]);
|
|
562
|
+
return {
|
|
563
|
+
// Recording state
|
|
564
|
+
isRecording,
|
|
565
|
+
isPaused,
|
|
566
|
+
duration,
|
|
567
|
+
level,
|
|
568
|
+
peakLevel,
|
|
569
|
+
error: hookError || micError || recError,
|
|
570
|
+
// Microphone state
|
|
571
|
+
stream,
|
|
572
|
+
devices,
|
|
573
|
+
hasPermission,
|
|
574
|
+
selectedDevice,
|
|
575
|
+
// Recording controls
|
|
576
|
+
startRecording,
|
|
577
|
+
stopRecording,
|
|
578
|
+
pauseRecording,
|
|
579
|
+
resumeRecording,
|
|
580
|
+
requestMicAccess,
|
|
581
|
+
changeDevice,
|
|
582
|
+
// Track state
|
|
583
|
+
recordingPeaks: peaks
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
441
587
|
// src/components/RecordButton.tsx
|
|
442
588
|
import styled from "styled-components";
|
|
443
589
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -702,6 +848,7 @@ export {
|
|
|
702
848
|
concatenateAudioData,
|
|
703
849
|
createAudioBuffer,
|
|
704
850
|
generatePeaks,
|
|
851
|
+
useIntegratedRecording,
|
|
705
852
|
useMicrophoneAccess,
|
|
706
853
|
useMicrophoneLevel,
|
|
707
854
|
useRecording
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/components/RecordButton.tsx","../src/components/MicrophoneSelector.tsx","../src/components/RecordingIndicator.tsx","../src/components/VUMeter.tsx"],"sourcesContent":["/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getContext } from 'tone';\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const {\n channelCount = 1,\n samplesPerPixel = 1024,\n } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [peaks, setPeaks] = useState<Int8Array | Int16Array>(new Int16Array(0));\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n const bits: 8 | 16 = 16; // Match the bit depth used by the final waveform\n\n // Global flag to prevent loading worklet multiple times\n // (AudioWorklet processors can only be registered once per AudioContext)\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const recordedChunksRef = useRef<Float32Array[]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getContext();\n // Load the worklet module\n // Use a relative path that works when bundled\n const workletUrl = new URL(\n './worklet/recording-processor.worklet.js',\n import.meta.url\n ).href;\n\n // Use Tone's addAudioWorkletModule for cross-browser compatibility\n await context.addAudioWorkletModule(workletUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.error('Failed to load AudioWorklet module:', err);\n throw new Error('Failed to load recording processor');\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Create AudioWorklet node using Tone's method\n const workletNode = context.createAudioWorkletNode('recording-processor');\n workletNodeRef.current = workletNode;\n\n // Connect source to worklet (but not to destination - no monitoring)\n source.connect(workletNode);\n\n //Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { samples } = event.data;\n\n // Accumulate samples\n recordedChunksRef.current.push(samples);\n totalSamplesRef.current += samples.length;\n\n // Update peaks incrementally for live waveform visualization\n setPeaks((prevPeaks) =>\n appendPeaks(\n prevPeaks,\n samples,\n samplesPerPixel,\n totalSamplesRef.current - samples.length,\n bits\n )\n );\n\n // Note: VU meter levels come from useMicrophoneLevel (AnalyserNode)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Start the worklet processor\n workletNode.port.postMessage({\n command: 'start',\n sampleRate: context.sampleRate,\n channelCount,\n });\n\n // Reset state\n recordedChunksRef.current = [];\n totalSamplesRef.current = 0;\n setPeaks(new Int16Array(0));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n\n // Start duration update loop\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n } catch (err) {\n console.error('Failed to start recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, loadWorklet, isRecording, isPaused]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecording) {\n return null;\n }\n\n try {\n // Stop the worklet\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (e) {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated chunks\n const allSamples = concatenateAudioData(recordedChunksRef.current);\n const context = getContext();\n // Use rawContext for createBuffer (native AudioContext method)\n const rawContext = context.rawContext as AudioContext;\n const buffer = createAudioBuffer(\n rawContext,\n allSamples,\n rawContext.sampleRate,\n channelCount\n );\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.error('Failed to stop recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n }\n }, [isRecording, channelCount]);\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n }\n }, [isRecording, isPaused, duration]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (e) {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert Float32Array to AudioBuffer\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n samples: Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n const buffer = audioContext.createBuffer(\n channelCount,\n samples.length,\n sampleRate\n );\n\n // Copy samples to buffer (for now, just mono)\n // Create a new Float32Array to ensure correct type\n const typedSamples = new Float32Array(samples);\n buffer.copyToChannel(typedSamples, 0);\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, newSamples, sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, combined, sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n peakArray[i * 2] = Math.floor(min * maxValue);\n peakArray[i * 2 + 1] = Math.floor(max * maxValue);\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.floor(min * maxValue);\n updated[existingPeaks.length - 1] = Math.floor(max * maxValue);\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(\n err instanceof Error ? err : new Error('Failed to access microphone')\n );\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n }, [stream, enumerateDevices]);\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state and enumerate devices\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Cleanup on unmount\n return () => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, []);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses Tone.js Meter for real-time audio level monitoring.\n */\n\nimport { useEffect, useState, useRef } from 'react';\nimport { Meter, getContext, connect } from 'tone';\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * FFT size for the analyser\n * Default: 256\n */\n fftSize?: number;\n\n /**\n * Smoothing time constant (0-1)\n * Higher values = smoother but slower response\n * Default: 0.8\n */\n smoothingTimeConstant?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current audio level (0-1)\n * 0 = silence, 1 = maximum level\n */\n level: number;\n\n /**\n * Peak level since last reset (0-1)\n */\n peakLevel: number;\n\n /**\n * Reset the peak level\n */\n resetPeak: () => void;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current level and peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { level, peakLevel, resetPeak } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const {\n updateRate = 60,\n smoothingTimeConstant = 0.8,\n } = options;\n\n const [level, setLevel] = useState(0);\n const [peakLevel, setPeakLevel] = useState(0);\n\n const meterRef = useRef<Meter | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const animationFrameRef = useRef<number | null>(null);\n\n const resetPeak = () => setPeakLevel(0);\n\n useEffect(() => {\n if (!stream) {\n setLevel(0);\n setPeakLevel(0);\n return;\n }\n\n let isMounted = true;\n\n // Setup audio monitoring\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n // Get Tone's context and resume if needed\n const context = getContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n if (!isMounted) return;\n\n // Create Tone.js Meter for level monitoring\n // Pass context to ensure it's created in the same context as the source\n const meter = new Meter({ smoothing: smoothingTimeConstant, context });\n meterRef.current = meter;\n\n // Create MediaStreamSource from the SAME context as the meter\n // Note: This creates a separate source from useRecording, but that's OK\n // since we're only using it for level monitoring (not recording)\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n\n // Connect source to meter using Tone's connect function\n connect(source, meter);\n\n // Start level monitoring\n const updateInterval = 1000 / updateRate;\n let lastUpdateTime = 0;\n\n const updateLevel = (timestamp: number) => {\n if (!isMounted || !meterRef.current) return;\n\n if (timestamp - lastUpdateTime >= updateInterval) {\n lastUpdateTime = timestamp;\n\n // Meter.getValue() returns dB, convert to 0-1 range\n const db = meterRef.current.getValue();\n const dbValue = typeof db === 'number' ? db : db[0];\n // dB is typically -Infinity to 0, map -100dB..0dB to 0..1\n // Using -100dB as floor since Firefox seems to report lower values\n const normalized = Math.max(0, Math.min(1, (dbValue + 100) / 100));\n\n setLevel(normalized);\n setPeakLevel(prev => Math.max(prev, normalized));\n }\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n setupMonitoring();\n\n // Cleanup\n return () => {\n isMounted = false;\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Disconnect and clean up\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (e) {\n // Ignore disconnect errors\n }\n sourceRef.current = null;\n }\n\n if (meterRef.current) {\n meterRef.current.dispose();\n meterRef.current = null;\n }\n };\n }, [stream, smoothingTimeConstant, updateRate]);\n\n return {\n level,\n peakLevel,\n resetPeak,\n };\n}\n","/**\n * RecordButton - Control button for starting/stopping recording\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordButtonProps {\n isRecording: boolean;\n onClick: () => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Button = styled.button<{ $isRecording: boolean }>`\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border: none;\n border-radius: 0.25rem;\n cursor: pointer;\n transition: all 0.2s ease-in-out;\n background: ${(props) => (props.$isRecording ? '#dc3545' : '#e74c3c')};\n color: white;\n\n &:hover:not(:disabled) {\n background: ${(props) => (props.$isRecording ? '#c82333' : '#c0392b')};\n transform: translateY(-1px);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n &:focus {\n outline: none;\n box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.3);\n }\n`;\n\nconst RecordingIndicator = styled.span`\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: white;\n margin-right: 0.5rem;\n animation: pulse 1.5s ease-in-out infinite;\n\n @keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n`;\n\nexport const RecordButton: React.FC<RecordButtonProps> = ({\n isRecording,\n onClick,\n disabled = false,\n className,\n}) => {\n return (\n <Button\n $isRecording={isRecording}\n onClick={onClick}\n disabled={disabled}\n className={className}\n aria-label={isRecording ? 'Stop recording' : 'Start recording'}\n >\n {isRecording && <RecordingIndicator />}\n {isRecording ? 'Stop Recording' : 'Record'}\n </Button>\n );\n};\n","/**\n * MicrophoneSelector - Dropdown for selecting microphone input device\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\nimport { BaseSelect, BaseLabel } from '@waveform-playlist/ui-components';\nimport { MicrophoneDevice } from '../types';\n\nexport interface MicrophoneSelectorProps {\n devices: MicrophoneDevice[];\n selectedDeviceId?: string;\n onDeviceChange: (deviceId: string) => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Select = styled(BaseSelect)`\n min-width: 200px;\n`;\n\nconst Label = styled(BaseLabel)`\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n`;\n\nexport const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = ({\n devices,\n selectedDeviceId,\n onDeviceChange,\n disabled = false,\n className,\n}) => {\n const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {\n onDeviceChange(event.target.value);\n };\n\n // Use first device if no selection provided\n const currentValue = selectedDeviceId || (devices.length > 0 ? devices[0].deviceId : '');\n\n return (\n <Label className={className}>\n Microphone\n <Select\n value={currentValue}\n onChange={handleChange}\n disabled={disabled || devices.length === 0}\n >\n {devices.length === 0 ? (\n <option value=\"\">No microphones found</option>\n ) : (\n devices.map((device) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))\n )}\n </Select>\n </Label>\n );\n};\n","/**\n * RecordingIndicator - Shows recording status, duration, and visual indicator\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordingIndicatorProps {\n isRecording: boolean;\n isPaused?: boolean;\n duration: number; // in seconds\n formatTime?: (seconds: number) => string;\n className?: string;\n}\n\nconst Container = styled.div<{ $isRecording: boolean }>`\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.5rem 0.75rem;\n background: ${(props) => (props.$isRecording ? '#fff3cd' : 'transparent')};\n border-radius: 0.25rem;\n transition: background 0.2s ease-in-out;\n`;\n\nconst Dot = styled.div<{ $isRecording: boolean; $isPaused: boolean }>`\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n opacity: ${(props) => (props.$isRecording ? 1 : 0)};\n transition: opacity 0.2s ease-in-out;\n\n ${(props) =>\n props.$isRecording &&\n !props.$isPaused &&\n `\n animation: blink 1.5s ease-in-out infinite;\n\n @keyframes blink {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n `}\n`;\n\nconst Duration = styled.span`\n font-family: 'Courier New', Monaco, monospace;\n font-size: 1rem;\n font-weight: 600;\n color: #495057;\n min-width: 70px;\n`;\n\nconst Status = styled.span<{ $isPaused: boolean }>`\n font-size: 0.75rem;\n font-weight: 500;\n color: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n text-transform: uppercase;\n`;\n\nconst defaultFormatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n};\n\nexport const RecordingIndicator: React.FC<RecordingIndicatorProps> = ({\n isRecording,\n isPaused = false,\n duration,\n formatTime = defaultFormatTime,\n className,\n}) => {\n return (\n <Container $isRecording={isRecording} className={className}>\n <Dot $isRecording={isRecording} $isPaused={isPaused} />\n <Duration>{formatTime(duration)}</Duration>\n {isRecording && <Status $isPaused={isPaused}>{isPaused ? 'Paused' : 'Recording'}</Status>}\n </Container>\n );\n};\n","/**\n * VU Meter Component\n *\n * Displays real-time audio input levels with color-coded zones\n * and peak indicator.\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface VUMeterProps {\n /**\n * Current audio level (0-1)\n */\n level: number;\n\n /**\n * Peak level (0-1)\n * Optional - if provided, shows peak indicator\n */\n peakLevel?: number;\n\n /**\n * Width of the meter in pixels\n * Default: 200\n */\n width?: number;\n\n /**\n * Height of the meter in pixels\n * Default: 20\n */\n height?: number;\n\n /**\n * Additional CSS class name\n */\n className?: string;\n}\n\nconst MeterContainer = styled.div<{ $width: number; $height: number }>`\n position: relative;\n width: ${props => props.$width}px;\n height: ${props => props.$height}px;\n background: #2c3e50;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);\n`;\n\n// Helper to get gradient color based on level\nconst getLevelGradient = (level: number): string => {\n if (level < 0.6) return 'linear-gradient(90deg, #27ae60, #2ecc71)';\n if (level < 0.85) return 'linear-gradient(90deg, #f39c12, #f1c40f)';\n return 'linear-gradient(90deg, #c0392b, #e74c3c)';\n};\n\n// Use .attrs() for frequently changing styles to avoid generating new CSS classes\nconst MeterFill = styled.div.attrs<{ $level: number; $height: number }>(props => ({\n style: {\n width: `${props.$level * 100}%`,\n height: `${props.$height}px`,\n background: getLevelGradient(props.$level),\n boxShadow: props.$level > 0.01 ? '0 0 8px rgba(255, 255, 255, 0.3)' : 'none',\n },\n}))<{ $level: number; $height: number }>`\n position: absolute;\n left: 0;\n top: 0;\n transition: width 0.05s ease-out, background 0.1s ease-out;\n`;\n\n// Use .attrs() for frequently changing left position\nconst PeakIndicator = styled.div.attrs<{ $peakLevel: number; $height: number }>(props => ({\n style: {\n left: `${props.$peakLevel * 100}%`,\n height: `${props.$height}px`,\n },\n}))<{ $peakLevel: number; $height: number }>`\n position: absolute;\n top: 0;\n width: 2px;\n background: #ecf0f1;\n box-shadow: 0 0 4px rgba(236, 240, 241, 0.8);\n transition: left 0.1s ease-out;\n`;\n\nconst ScaleMarkers = styled.div<{ $height: number }>`\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: ${props => props.$height}px;\n pointer-events: none;\n`;\n\nconst ScaleMark = styled.div<{ $position: number; $height: number }>`\n position: absolute;\n left: ${props => props.$position}%;\n top: 0;\n width: 1px;\n height: ${props => props.$height}px;\n background: rgba(255, 255, 255, 0.2);\n`;\n\n/**\n * VU Meter component for displaying audio input levels\n *\n * @example\n * ```typescript\n * import { useMicrophoneLevel, VUMeter } from '@waveform-playlist/recording';\n *\n * const { level, peakLevel } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} width={300} height={24} />;\n * ```\n */\nconst VUMeterComponent: React.FC<VUMeterProps> = ({\n level,\n peakLevel,\n width = 200,\n height = 20,\n className,\n}) => {\n // Clamp values to 0-1 range\n const clampedLevel = Math.max(0, Math.min(1, level));\n const clampedPeak = peakLevel !== undefined\n ? Math.max(0, Math.min(1, peakLevel))\n : 0;\n\n return (\n <MeterContainer $width={width} $height={height} className={className}>\n <MeterFill $level={clampedLevel} $height={height} />\n\n {peakLevel !== undefined && clampedPeak > 0 && (\n <PeakIndicator $peakLevel={clampedPeak} $height={height} />\n )}\n\n <ScaleMarkers $height={height}>\n <ScaleMark $position={60} $height={height} />\n <ScaleMark $position={85} $height={height} />\n </ScaleMarkers>\n </MeterContainer>\n );\n};\n\n// Memoize to prevent unnecessary re-renders when parent updates\nexport const VUMeter = React.memo(VUMeterComponent);\n"],"mappings":"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","names":["newPeaks","result","useState","useEffect","useCallback","useEffect","useState","useRef","getContext","styled","jsx","jsxs","styled","jsx","jsxs","RecordingIndicator","styled","jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useRecording.ts","../src/utils/audioBufferUtils.ts","../src/utils/peaksGenerator.ts","../src/hooks/useMicrophoneAccess.ts","../src/hooks/useMicrophoneLevel.ts","../src/hooks/useIntegratedRecording.ts","../src/components/RecordButton.tsx","../src/components/MicrophoneSelector.tsx","../src/components/RecordingIndicator.tsx","../src/components/VUMeter.tsx"],"sourcesContent":["/**\n * Main recording hook using AudioWorklet\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { UseRecordingReturn, RecordingOptions } from '../types';\nimport { concatenateAudioData, createAudioBuffer } from '../utils/audioBufferUtils';\nimport { appendPeaks } from '../utils/peaksGenerator';\nimport { getContext } from 'tone';\n\nexport function useRecording(\n stream: MediaStream | null,\n options: RecordingOptions = {}\n): UseRecordingReturn {\n const {\n channelCount = 1,\n samplesPerPixel = 1024,\n } = options;\n\n // State\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [peaks, setPeaks] = useState<Int8Array | Int16Array>(new Int16Array(0));\n const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [level, setLevel] = useState(0); // Current RMS level (0-1)\n const [peakLevel, setPeakLevel] = useState(0); // Peak level since recording started (0-1)\n\n const bits: 8 | 16 = 16; // Match the bit depth used by the final waveform\n\n // Global flag to prevent loading worklet multiple times\n // (AudioWorklet processors can only be registered once per AudioContext)\n const workletLoadedRef = useRef<boolean>(false);\n\n // Refs for AudioWorklet and data accumulation\n const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n const mediaStreamSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const recordedChunksRef = useRef<Float32Array[]>([]);\n const totalSamplesRef = useRef(0);\n const animationFrameRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const isRecordingRef = useRef<boolean>(false);\n const isPausedRef = useRef<boolean>(false);\n\n // Load AudioWorklet module\n const loadWorklet = useCallback(async () => {\n // Skip if already loaded to prevent \"already registered\" error\n if (workletLoadedRef.current) {\n return;\n }\n\n try {\n const context = getContext();\n // Load the worklet module\n // Use a relative path that works when bundled\n const workletUrl = new URL(\n './worklet/recording-processor.worklet.js',\n import.meta.url\n ).href;\n\n // Use Tone's addAudioWorkletModule for cross-browser compatibility\n await context.addAudioWorkletModule(workletUrl);\n workletLoadedRef.current = true;\n } catch (err) {\n console.error('Failed to load AudioWorklet module:', err);\n throw new Error('Failed to load recording processor');\n }\n }, []);\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (!stream) {\n setError(new Error('No microphone stream available'));\n return;\n }\n\n try {\n setError(null);\n\n // Use Tone.js Context for cross-browser compatibility\n const context = getContext();\n\n // Resume AudioContext if suspended\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n // Load worklet module\n await loadWorklet();\n\n // Create MediaStreamSource from Tone's context\n // Each hook creates its own source to avoid cross-context issues in Firefox\n const source = context.createMediaStreamSource(stream);\n mediaStreamSourceRef.current = source;\n\n // Create AudioWorklet node using Tone's method\n const workletNode = context.createAudioWorkletNode('recording-processor');\n workletNodeRef.current = workletNode;\n\n // Connect source to worklet (but not to destination - no monitoring)\n source.connect(workletNode);\n\n //Listen for audio data from worklet\n workletNode.port.onmessage = (event: MessageEvent) => {\n const { samples } = event.data;\n\n // Accumulate samples\n recordedChunksRef.current.push(samples);\n totalSamplesRef.current += samples.length;\n\n // Update peaks incrementally for live waveform visualization\n setPeaks((prevPeaks) =>\n appendPeaks(\n prevPeaks,\n samples,\n samplesPerPixel,\n totalSamplesRef.current - samples.length,\n bits\n )\n );\n\n // Note: VU meter levels come from useMicrophoneLevel (AnalyserNode)\n // We don't update level/peakLevel here to avoid conflicting state updates\n };\n\n // Start the worklet processor\n workletNode.port.postMessage({\n command: 'start',\n sampleRate: context.sampleRate,\n channelCount,\n });\n\n // Reset state\n recordedChunksRef.current = [];\n totalSamplesRef.current = 0;\n setPeaks(new Int16Array(0));\n setAudioBuffer(null);\n setLevel(0);\n setPeakLevel(0);\n isRecordingRef.current = true;\n isPausedRef.current = false;\n setIsRecording(true);\n setIsPaused(false);\n startTimeRef.current = performance.now();\n\n // Start duration update loop\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n } catch (err) {\n console.error('Failed to start recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to start recording'));\n }\n }, [stream, channelCount, samplesPerPixel, loadWorklet, isRecording, isPaused]);\n\n // Stop recording\n const stopRecording = useCallback(async (): Promise<AudioBuffer | null> => {\n if (!isRecording) {\n return null;\n }\n\n try {\n // Stop the worklet\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (e) {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\n }\n\n // Cancel animation frame\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Create final AudioBuffer from accumulated chunks\n const allSamples = concatenateAudioData(recordedChunksRef.current);\n const context = getContext();\n // Use rawContext for createBuffer (native AudioContext method)\n const rawContext = context.rawContext as AudioContext;\n const buffer = createAudioBuffer(\n rawContext,\n allSamples,\n rawContext.sampleRate,\n channelCount\n );\n\n setAudioBuffer(buffer);\n setDuration(buffer.duration);\n isRecordingRef.current = false;\n isPausedRef.current = false;\n setIsRecording(false);\n setIsPaused(false);\n setLevel(0);\n // Keep peakLevel to show the peak reached during recording\n\n return buffer;\n } catch (err) {\n console.error('Failed to stop recording:', err);\n setError(err instanceof Error ? err : new Error('Failed to stop recording'));\n return null;\n }\n }, [isRecording, channelCount]);\n\n // Pause recording\n const pauseRecording = useCallback(() => {\n if (isRecording && !isPaused) {\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n isPausedRef.current = true;\n setIsPaused(true);\n }\n }, [isRecording, isPaused]);\n\n // Resume recording\n const resumeRecording = useCallback(() => {\n if (isRecording && isPaused) {\n isPausedRef.current = false;\n setIsPaused(false);\n startTimeRef.current = performance.now() - duration * 1000;\n\n const updateDuration = () => {\n if (isRecordingRef.current && !isPausedRef.current) {\n const elapsed = (performance.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n animationFrameRef.current = requestAnimationFrame(updateDuration);\n }\n };\n updateDuration();\n }\n }, [isRecording, isPaused, duration]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (workletNodeRef.current) {\n workletNodeRef.current.port.postMessage({ command: 'stop' });\n\n // Disconnect worklet from source\n if (mediaStreamSourceRef.current) {\n try {\n mediaStreamSourceRef.current.disconnect(workletNodeRef.current);\n } catch (e) {\n // Source may have already been disconnected when stream changed\n // This is fine - just ignore the error\n }\n }\n workletNodeRef.current.disconnect();\n }\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n // Don't close the global AudioContext - it's shared across the app\n };\n }, []);\n\n return {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer,\n level,\n peakLevel,\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n error,\n };\n}\n","/**\n * Utility functions for working with AudioBuffers during recording\n */\n\n/**\n * Concatenate multiple Float32Arrays into a single array\n */\nexport function concatenateAudioData(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const result = new Float32Array(totalLength);\n\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n/**\n * Convert Float32Array to AudioBuffer\n */\nexport function createAudioBuffer(\n audioContext: AudioContext,\n samples: Float32Array,\n sampleRate: number,\n channelCount: number = 1\n): AudioBuffer {\n const buffer = audioContext.createBuffer(\n channelCount,\n samples.length,\n sampleRate\n );\n\n // Copy samples to buffer (for now, just mono)\n // Create a new Float32Array to ensure correct type\n const typedSamples = new Float32Array(samples);\n buffer.copyToChannel(typedSamples, 0);\n\n return buffer;\n}\n\n/**\n * Append new samples to an existing AudioBuffer\n */\nexport function appendToAudioBuffer(\n audioContext: AudioContext,\n existingBuffer: AudioBuffer | null,\n newSamples: Float32Array,\n sampleRate: number\n): AudioBuffer {\n if (!existingBuffer) {\n return createAudioBuffer(audioContext, newSamples, sampleRate);\n }\n\n // Get existing samples\n const existingData = existingBuffer.getChannelData(0);\n\n // Concatenate using concatenateAudioData helper\n const combined = concatenateAudioData([existingData, newSamples]);\n\n // Create new buffer\n return createAudioBuffer(audioContext, combined, sampleRate);\n}\n\n/**\n * Calculate duration in seconds from sample count and sample rate\n */\nexport function calculateDuration(sampleCount: number, sampleRate: number): number {\n return sampleCount / sampleRate;\n}\n","/**\n * Peak generation for real-time waveform visualization during recording\n * Matches the format used by webaudio-peaks: min/max pairs with bit depth\n */\n\n/**\n * Generate peaks from audio samples in standard min/max pair format\n *\n * @param samples - Audio samples to process\n * @param samplesPerPixel - Number of samples to represent in each peak\n * @param bits - Bit depth for peak values (8 or 16)\n * @returns Int8Array or Int16Array of peak values (min/max pairs)\n */\nexport function generatePeaks(\n samples: Float32Array,\n samplesPerPixel: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const numPeaks = Math.ceil(samples.length / samplesPerPixel);\n const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);\n const maxValue = 2 ** (bits - 1);\n\n for (let i = 0; i < numPeaks; i++) {\n const start = i * samplesPerPixel;\n const end = Math.min(start + samplesPerPixel, samples.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j++) {\n const value = samples[j];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Store as min/max pairs scaled to bit depth\n peakArray[i * 2] = Math.floor(min * maxValue);\n peakArray[i * 2 + 1] = Math.floor(max * maxValue);\n }\n\n return peakArray;\n}\n\n/**\n * Append new peaks to existing peaks array\n * This is used for incremental peak updates during recording\n */\nexport function appendPeaks(\n existingPeaks: Int8Array | Int16Array,\n newSamples: Float32Array,\n samplesPerPixel: number,\n totalSamplesProcessed: number,\n bits: 8 | 16 = 16\n): Int8Array | Int16Array {\n const maxValue = 2 ** (bits - 1);\n\n // Check if we have a partial peak from the last update\n const remainder = totalSamplesProcessed % samplesPerPixel;\n let offset = 0;\n\n // If there's a partial peak, we need to update the last peak\n if (remainder > 0 && existingPeaks.length > 0) {\n const samplesToComplete = samplesPerPixel - remainder;\n const endIndex = Math.min(samplesToComplete, newSamples.length);\n\n // Get current min/max from last peak\n let min = existingPeaks[existingPeaks.length - 2] / maxValue;\n let max = existingPeaks[existingPeaks.length - 1] / maxValue;\n\n // Update with new samples\n for (let i = 0; i < endIndex; i++) {\n const value = newSamples[i];\n if (value < min) min = value;\n if (value > max) max = value;\n }\n\n // Update last peak\n const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);\n updated.set(existingPeaks);\n updated[existingPeaks.length - 2] = Math.floor(min * maxValue);\n updated[existingPeaks.length - 1] = Math.floor(max * maxValue);\n\n offset = endIndex;\n\n // Generate peaks for remaining samples and concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks.length);\n result.set(updated);\n result.set(newPeaks, updated.length);\n return result;\n }\n\n // No partial peak, just concatenate\n const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);\n const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);\n result.set(existingPeaks);\n result.set(newPeaks, existingPeaks.length);\n return result;\n}\n","/**\n * Hook for managing microphone access and device enumeration\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { UseMicrophoneAccessReturn, MicrophoneDevice } from '../types';\n\nexport function useMicrophoneAccess(): UseMicrophoneAccessReturn {\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [devices, setDevices] = useState<MicrophoneDevice[]>([]);\n const [hasPermission, setHasPermission] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Enumerate audio input devices\n const enumerateDevices = useCallback(async () => {\n try {\n const allDevices = await navigator.mediaDevices.enumerateDevices();\n const audioInputs = allDevices\n .filter((device) => device.kind === 'audioinput')\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,\n groupId: device.groupId,\n }));\n\n setDevices(audioInputs);\n } catch (err) {\n console.error('Failed to enumerate devices:', err);\n setError(err instanceof Error ? err : new Error('Failed to enumerate devices'));\n }\n }, []);\n\n // Request microphone access\n const requestAccess = useCallback(async (deviceId?: string, audioConstraints?: MediaTrackConstraints) => {\n setIsLoading(true);\n setError(null);\n\n try {\n // Stop existing stream if any\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n\n // Build audio constraints\n const audio: MediaTrackConstraints & { latency?: number } = {\n // Recording-optimized defaults: prioritize raw audio quality and low latency\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n latency: 0, // Low latency mode (not in TS types yet, but supported in modern browsers)\n // User-provided constraints override defaults\n ...audioConstraints,\n // Device ID override (if specified)\n ...(deviceId && { deviceId: { exact: deviceId } }),\n };\n\n const constraints: MediaStreamConstraints = {\n audio,\n video: false,\n };\n\n const newStream = await navigator.mediaDevices.getUserMedia(constraints);\n setStream(newStream);\n setHasPermission(true);\n\n // Enumerate devices after getting permission (labels will be available)\n await enumerateDevices();\n } catch (err) {\n console.error('Failed to access microphone:', err);\n setError(\n err instanceof Error ? err : new Error('Failed to access microphone')\n );\n setHasPermission(false);\n } finally {\n setIsLoading(false);\n }\n }, [stream, enumerateDevices]);\n\n // Stop the stream and revoke access\n const stopStream = useCallback(() => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n setStream(null);\n setHasPermission(false);\n }\n }, [stream]);\n\n // Check initial permission state and enumerate devices\n useEffect(() => {\n // Try to enumerate devices (labels won't be available without permission)\n enumerateDevices();\n\n // Cleanup on unmount\n return () => {\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n };\n }, []);\n\n return {\n stream,\n devices,\n hasPermission,\n isLoading,\n requestAccess,\n stopStream,\n error,\n };\n}\n","/**\n * Hook for monitoring microphone input levels\n *\n * Uses Tone.js Meter for real-time audio level monitoring.\n */\n\nimport { useEffect, useState, useRef } from 'react';\nimport { Meter, getContext, connect } from 'tone';\n\nexport interface UseMicrophoneLevelOptions {\n /**\n * How often to update the level (in Hz)\n * Default: 60 (60fps)\n */\n updateRate?: number;\n\n /**\n * FFT size for the analyser\n * Default: 256\n */\n fftSize?: number;\n\n /**\n * Smoothing time constant (0-1)\n * Higher values = smoother but slower response\n * Default: 0.8\n */\n smoothingTimeConstant?: number;\n}\n\nexport interface UseMicrophoneLevelReturn {\n /**\n * Current audio level (0-1)\n * 0 = silence, 1 = maximum level\n */\n level: number;\n\n /**\n * Peak level since last reset (0-1)\n */\n peakLevel: number;\n\n /**\n * Reset the peak level\n */\n resetPeak: () => void;\n}\n\n/**\n * Monitor microphone input levels in real-time\n *\n * @param stream - MediaStream from getUserMedia\n * @param options - Configuration options\n * @returns Object with current level and peak level\n *\n * @example\n * ```typescript\n * const { stream } = useMicrophoneAccess();\n * const { level, peakLevel, resetPeak } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} />;\n * ```\n */\nexport function useMicrophoneLevel(\n stream: MediaStream | null,\n options: UseMicrophoneLevelOptions = {}\n): UseMicrophoneLevelReturn {\n const {\n updateRate = 60,\n smoothingTimeConstant = 0.8,\n } = options;\n\n const [level, setLevel] = useState(0);\n const [peakLevel, setPeakLevel] = useState(0);\n\n const meterRef = useRef<Meter | null>(null);\n const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const animationFrameRef = useRef<number | null>(null);\n\n const resetPeak = () => setPeakLevel(0);\n\n useEffect(() => {\n if (!stream) {\n setLevel(0);\n setPeakLevel(0);\n return;\n }\n\n let isMounted = true;\n\n // Setup audio monitoring\n const setupMonitoring = async () => {\n if (!isMounted) return;\n\n // Get Tone's context and resume if needed\n const context = getContext();\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n if (!isMounted) return;\n\n // Create Tone.js Meter for level monitoring\n // Pass context to ensure it's created in the same context as the source\n const meter = new Meter({ smoothing: smoothingTimeConstant, context });\n meterRef.current = meter;\n\n // Create MediaStreamSource from the SAME context as the meter\n // Note: This creates a separate source from useRecording, but that's OK\n // since we're only using it for level monitoring (not recording)\n const source = context.createMediaStreamSource(stream);\n sourceRef.current = source;\n\n // Connect source to meter using Tone's connect function\n connect(source, meter);\n\n // Start level monitoring\n const updateInterval = 1000 / updateRate;\n let lastUpdateTime = 0;\n\n const updateLevel = (timestamp: number) => {\n if (!isMounted || !meterRef.current) return;\n\n if (timestamp - lastUpdateTime >= updateInterval) {\n lastUpdateTime = timestamp;\n\n // Meter.getValue() returns dB, convert to 0-1 range\n const db = meterRef.current.getValue();\n const dbValue = typeof db === 'number' ? db : db[0];\n // dB is typically -Infinity to 0, map -100dB..0dB to 0..1\n // Using -100dB as floor since Firefox seems to report lower values\n const normalized = Math.max(0, Math.min(1, (dbValue + 100) / 100));\n\n setLevel(normalized);\n setPeakLevel(prev => Math.max(prev, normalized));\n }\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n\n setupMonitoring();\n\n // Cleanup\n return () => {\n isMounted = false;\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n // Disconnect and clean up\n if (sourceRef.current) {\n try {\n sourceRef.current.disconnect();\n } catch (e) {\n // Ignore disconnect errors\n }\n sourceRef.current = null;\n }\n\n if (meterRef.current) {\n meterRef.current.dispose();\n meterRef.current = null;\n }\n };\n }, [stream, smoothingTimeConstant, updateRate]);\n\n return {\n level,\n peakLevel,\n resetPeak,\n };\n}\n","/**\n * Hook for integrated multi-track recording\n * Combines recording functionality with track management\n */\n\nimport { useState, useCallback, useEffect } from 'react';\nimport { useRecording } from './useRecording';\nimport { useMicrophoneAccess } from './useMicrophoneAccess';\nimport { useMicrophoneLevel } from './useMicrophoneLevel';\nimport type { MicrophoneDevice } from '../types';\nimport { type ClipTrack, type AudioClip } from '@waveform-playlist/core';\nimport { resumeGlobalAudioContext } from '@waveform-playlist/playout';\n\nexport interface IntegratedRecordingOptions {\n /**\n * Current playback/cursor position in seconds\n * Recording will start from max(currentTime, lastClipEndTime)\n */\n currentTime?: number;\n\n /**\n * MediaTrackConstraints for audio recording\n * These will override the recording-optimized defaults (echo cancellation off, low latency)\n */\n audioConstraints?: MediaTrackConstraints;\n\n /**\n * Number of channels to record (1 = mono, 2 = stereo)\n * Default: 1 (mono)\n */\n channelCount?: number;\n\n /**\n * Samples per pixel for peak generation\n * Default: 1024\n */\n samplesPerPixel?: number;\n}\n\nexport interface UseIntegratedRecordingReturn {\n // Recording state\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n level: number;\n peakLevel: number;\n error: Error | null;\n\n // Microphone state\n stream: MediaStream | null;\n devices: MicrophoneDevice[];\n hasPermission: boolean;\n selectedDevice: string | null;\n\n // Recording controls\n startRecording: () => void;\n stopRecording: () => void;\n pauseRecording: () => void;\n resumeRecording: () => void;\n requestMicAccess: () => Promise<void>;\n changeDevice: (deviceId: string) => Promise<void>;\n\n // Track state (for live waveform during recording)\n recordingPeaks: Int8Array | Int16Array;\n}\n\nexport function useIntegratedRecording(\n tracks: ClipTrack[],\n setTracks: (tracks: ClipTrack[]) => void,\n selectedTrackId: string | null,\n options: IntegratedRecordingOptions = {}\n): UseIntegratedRecordingReturn {\n const { currentTime = 0, audioConstraints, ...recordingOptions } = options;\n\n // Track if we're currently monitoring (for auto-resume audio context)\n const [isMonitoring, setIsMonitoring] = useState(false);\n const [selectedDevice, setSelectedDevice] = useState<string | null>(null);\n const [hookError, setHookError] = useState<Error | null>(null);\n\n // Microphone access\n const {\n stream,\n devices,\n hasPermission,\n requestAccess,\n error: micError,\n } = useMicrophoneAccess();\n\n // Microphone level (for VU meter)\n const { level, peakLevel } = useMicrophoneLevel(stream);\n\n // Recording\n const {\n isRecording,\n isPaused,\n duration,\n peaks,\n audioBuffer: _audioBuffer,\n startRecording: startRec,\n stopRecording: stopRec,\n pauseRecording,\n resumeRecording,\n error: recError,\n } = useRecording(stream, recordingOptions);\n\n // Start recording handler\n const startRecording = useCallback(async () => {\n if (!selectedTrackId) {\n setHookError(new Error('Cannot start recording: no track selected. Select or create a track first.'));\n return;\n }\n\n try {\n setHookError(null);\n // Resume audio context if needed\n if (!isMonitoring) {\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n }\n\n await startRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [selectedTrackId, isMonitoring, startRec]);\n\n // Stop recording and add clip to selected track\n const stopRecording = useCallback(async () => {\n let buffer: AudioBuffer | null;\n try {\n buffer = await stopRec();\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n // Add clip to track after recording completes\n if (buffer && selectedTrackId) {\n const selectedTrackIndex = tracks.findIndex(t => t.id === selectedTrackId);\n if (selectedTrackIndex === -1) {\n const err = new Error(\n `Recording completed but track \"${selectedTrackId}\" no longer exists. The recorded audio could not be saved.`\n );\n console.error(`[waveform-playlist] ${err.message}`);\n setHookError(err);\n return;\n }\n\n const selectedTrack = tracks[selectedTrackIndex];\n\n // Calculate start position: max(currentTime, lastClipEndTime)\n const currentTimeSamples = Math.floor(currentTime * buffer.sampleRate);\n\n let lastClipEndSample = 0;\n if (selectedTrack.clips.length > 0) {\n // Find the end time of the last clip (in samples)\n const endSamples = selectedTrack.clips.map(clip =>\n clip.startSample + clip.durationSamples\n );\n lastClipEndSample = Math.max(...endSamples);\n }\n\n // Use whichever is greater: cursor position or last clip end\n const startSample = Math.max(currentTimeSamples, lastClipEndSample);\n\n // Create new clip from recording\n const newClip: AudioClip = {\n id: `clip-${Date.now()}`,\n audioBuffer: buffer,\n startSample,\n durationSamples: buffer.length,\n offsetSamples: 0,\n sampleRate: buffer.sampleRate,\n sourceDurationSamples: buffer.length,\n gain: 1.0,\n name: `Recording ${new Date().toLocaleTimeString()}`,\n };\n\n // Add clip to track\n const newTracks = tracks.map((track, index) => {\n if (index === selectedTrackIndex) {\n return {\n ...track,\n clips: [...track.clips, newClip],\n };\n }\n return track;\n });\n\n setTracks(newTracks);\n }\n }, [selectedTrackId, tracks, setTracks, currentTime, stopRec]);\n\n // Auto-select the first device when devices become available\n useEffect(() => {\n // Only auto-select if we have permission, devices are available, and nothing is selected yet\n if (hasPermission && devices.length > 0 && selectedDevice === null) {\n setSelectedDevice(devices[0].deviceId);\n }\n }, [hasPermission, devices.length]); // Intentionally not including selectedDevice to avoid re-triggering\n\n // Request microphone access\n const requestMicAccess = useCallback(async () => {\n try {\n setHookError(null);\n await requestAccess(undefined, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n // Change device\n const changeDevice = useCallback(async (deviceId: string) => {\n try {\n setHookError(null);\n setSelectedDevice(deviceId);\n await requestAccess(deviceId, audioConstraints);\n await resumeGlobalAudioContext();\n setIsMonitoring(true);\n } catch (err) {\n setHookError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [requestAccess, audioConstraints]);\n\n return {\n // Recording state\n isRecording,\n isPaused,\n duration,\n level,\n peakLevel,\n error: hookError || micError || recError,\n\n // Microphone state\n stream,\n devices,\n hasPermission,\n selectedDevice,\n\n // Recording controls\n startRecording,\n stopRecording,\n pauseRecording,\n resumeRecording,\n requestMicAccess,\n changeDevice,\n\n // Track state\n recordingPeaks: peaks,\n };\n}\n","/**\n * RecordButton - Control button for starting/stopping recording\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordButtonProps {\n isRecording: boolean;\n onClick: () => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Button = styled.button<{ $isRecording: boolean }>`\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border: none;\n border-radius: 0.25rem;\n cursor: pointer;\n transition: all 0.2s ease-in-out;\n background: ${(props) => (props.$isRecording ? '#dc3545' : '#e74c3c')};\n color: white;\n\n &:hover:not(:disabled) {\n background: ${(props) => (props.$isRecording ? '#c82333' : '#c0392b')};\n transform: translateY(-1px);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n &:focus {\n outline: none;\n box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.3);\n }\n`;\n\nconst RecordingIndicator = styled.span`\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: white;\n margin-right: 0.5rem;\n animation: pulse 1.5s ease-in-out infinite;\n\n @keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n`;\n\nexport const RecordButton: React.FC<RecordButtonProps> = ({\n isRecording,\n onClick,\n disabled = false,\n className,\n}) => {\n return (\n <Button\n $isRecording={isRecording}\n onClick={onClick}\n disabled={disabled}\n className={className}\n aria-label={isRecording ? 'Stop recording' : 'Start recording'}\n >\n {isRecording && <RecordingIndicator />}\n {isRecording ? 'Stop Recording' : 'Record'}\n </Button>\n );\n};\n","/**\n * MicrophoneSelector - Dropdown for selecting microphone input device\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\nimport { BaseSelect, BaseLabel } from '@waveform-playlist/ui-components';\nimport { MicrophoneDevice } from '../types';\n\nexport interface MicrophoneSelectorProps {\n devices: MicrophoneDevice[];\n selectedDeviceId?: string;\n onDeviceChange: (deviceId: string) => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst Select = styled(BaseSelect)`\n min-width: 200px;\n`;\n\nconst Label = styled(BaseLabel)`\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n`;\n\nexport const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = ({\n devices,\n selectedDeviceId,\n onDeviceChange,\n disabled = false,\n className,\n}) => {\n const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {\n onDeviceChange(event.target.value);\n };\n\n // Use first device if no selection provided\n const currentValue = selectedDeviceId || (devices.length > 0 ? devices[0].deviceId : '');\n\n return (\n <Label className={className}>\n Microphone\n <Select\n value={currentValue}\n onChange={handleChange}\n disabled={disabled || devices.length === 0}\n >\n {devices.length === 0 ? (\n <option value=\"\">No microphones found</option>\n ) : (\n devices.map((device) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))\n )}\n </Select>\n </Label>\n );\n};\n","/**\n * RecordingIndicator - Shows recording status, duration, and visual indicator\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface RecordingIndicatorProps {\n isRecording: boolean;\n isPaused?: boolean;\n duration: number; // in seconds\n formatTime?: (seconds: number) => string;\n className?: string;\n}\n\nconst Container = styled.div<{ $isRecording: boolean }>`\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.5rem 0.75rem;\n background: ${(props) => (props.$isRecording ? '#fff3cd' : 'transparent')};\n border-radius: 0.25rem;\n transition: background 0.2s ease-in-out;\n`;\n\nconst Dot = styled.div<{ $isRecording: boolean; $isPaused: boolean }>`\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n opacity: ${(props) => (props.$isRecording ? 1 : 0)};\n transition: opacity 0.2s ease-in-out;\n\n ${(props) =>\n props.$isRecording &&\n !props.$isPaused &&\n `\n animation: blink 1.5s ease-in-out infinite;\n\n @keyframes blink {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n `}\n`;\n\nconst Duration = styled.span`\n font-family: 'Courier New', Monaco, monospace;\n font-size: 1rem;\n font-weight: 600;\n color: #495057;\n min-width: 70px;\n`;\n\nconst Status = styled.span<{ $isPaused: boolean }>`\n font-size: 0.75rem;\n font-weight: 500;\n color: ${(props) => (props.$isPaused ? '#ffc107' : '#dc3545')};\n text-transform: uppercase;\n`;\n\nconst defaultFormatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n};\n\nexport const RecordingIndicator: React.FC<RecordingIndicatorProps> = ({\n isRecording,\n isPaused = false,\n duration,\n formatTime = defaultFormatTime,\n className,\n}) => {\n return (\n <Container $isRecording={isRecording} className={className}>\n <Dot $isRecording={isRecording} $isPaused={isPaused} />\n <Duration>{formatTime(duration)}</Duration>\n {isRecording && <Status $isPaused={isPaused}>{isPaused ? 'Paused' : 'Recording'}</Status>}\n </Container>\n );\n};\n","/**\n * VU Meter Component\n *\n * Displays real-time audio input levels with color-coded zones\n * and peak indicator.\n */\n\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport interface VUMeterProps {\n /**\n * Current audio level (0-1)\n */\n level: number;\n\n /**\n * Peak level (0-1)\n * Optional - if provided, shows peak indicator\n */\n peakLevel?: number;\n\n /**\n * Width of the meter in pixels\n * Default: 200\n */\n width?: number;\n\n /**\n * Height of the meter in pixels\n * Default: 20\n */\n height?: number;\n\n /**\n * Additional CSS class name\n */\n className?: string;\n}\n\nconst MeterContainer = styled.div<{ $width: number; $height: number }>`\n position: relative;\n width: ${props => props.$width}px;\n height: ${props => props.$height}px;\n background: #2c3e50;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);\n`;\n\n// Helper to get gradient color based on level\nconst getLevelGradient = (level: number): string => {\n if (level < 0.6) return 'linear-gradient(90deg, #27ae60, #2ecc71)';\n if (level < 0.85) return 'linear-gradient(90deg, #f39c12, #f1c40f)';\n return 'linear-gradient(90deg, #c0392b, #e74c3c)';\n};\n\n// Use .attrs() for frequently changing styles to avoid generating new CSS classes\nconst MeterFill = styled.div.attrs<{ $level: number; $height: number }>(props => ({\n style: {\n width: `${props.$level * 100}%`,\n height: `${props.$height}px`,\n background: getLevelGradient(props.$level),\n boxShadow: props.$level > 0.01 ? '0 0 8px rgba(255, 255, 255, 0.3)' : 'none',\n },\n}))<{ $level: number; $height: number }>`\n position: absolute;\n left: 0;\n top: 0;\n transition: width 0.05s ease-out, background 0.1s ease-out;\n`;\n\n// Use .attrs() for frequently changing left position\nconst PeakIndicator = styled.div.attrs<{ $peakLevel: number; $height: number }>(props => ({\n style: {\n left: `${props.$peakLevel * 100}%`,\n height: `${props.$height}px`,\n },\n}))<{ $peakLevel: number; $height: number }>`\n position: absolute;\n top: 0;\n width: 2px;\n background: #ecf0f1;\n box-shadow: 0 0 4px rgba(236, 240, 241, 0.8);\n transition: left 0.1s ease-out;\n`;\n\nconst ScaleMarkers = styled.div<{ $height: number }>`\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: ${props => props.$height}px;\n pointer-events: none;\n`;\n\nconst ScaleMark = styled.div<{ $position: number; $height: number }>`\n position: absolute;\n left: ${props => props.$position}%;\n top: 0;\n width: 1px;\n height: ${props => props.$height}px;\n background: rgba(255, 255, 255, 0.2);\n`;\n\n/**\n * VU Meter component for displaying audio input levels\n *\n * @example\n * ```typescript\n * import { useMicrophoneLevel, VUMeter } from '@waveform-playlist/recording';\n *\n * const { level, peakLevel } = useMicrophoneLevel(stream);\n *\n * return <VUMeter level={level} peakLevel={peakLevel} width={300} height={24} />;\n * ```\n */\nconst VUMeterComponent: React.FC<VUMeterProps> = ({\n level,\n peakLevel,\n width = 200,\n height = 20,\n className,\n}) => {\n // Clamp values to 0-1 range\n const clampedLevel = Math.max(0, Math.min(1, level));\n const clampedPeak = peakLevel !== undefined\n ? Math.max(0, Math.min(1, peakLevel))\n : 0;\n\n return (\n <MeterContainer $width={width} $height={height} className={className}>\n <MeterFill $level={clampedLevel} $height={height} />\n\n {peakLevel !== undefined && clampedPeak > 0 && (\n <PeakIndicator $peakLevel={clampedPeak} $height={height} />\n )}\n\n <ScaleMarkers $height={height}>\n <ScaleMark $position={60} $height={height} />\n <ScaleMark $position={85} $height={height} />\n </ScaleMarkers>\n </MeterContainer>\n );\n};\n\n// Memoize to prevent unnecessary re-renders when parent updates\nexport const VUMeter = React.memo(VUMeterComponent);\n"],"mappings":"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","names":["newPeaks","result","useState","useEffect","useCallback","useEffect","useState","useRef","getContext","useState","useCallback","useEffect","useState","useCallback","useEffect","styled","jsx","jsxs","styled","jsx","jsxs","RecordingIndicator","styled","jsx","jsxs"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveform-playlist/recording",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Audio recording support for waveform-playlist using AudioWorklet",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"typescript": "^5.3.3"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@waveform-playlist/core": "
|
|
46
|
-
"@waveform-playlist/playout": "
|
|
47
|
-
"@waveform-playlist/ui-components": "
|
|
45
|
+
"@waveform-playlist/core": "6.0.0",
|
|
46
|
+
"@waveform-playlist/playout": "6.0.0",
|
|
47
|
+
"@waveform-playlist/ui-components": "6.0.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^18.0.0",
|