@waveform-playlist/recording 12.2.0 → 13.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/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # @waveform-playlist/recording
2
2
 
3
- Audio recording support for waveform-playlist using AudioWorklet.
3
+ Audio recording support for waveform-playlist using AudioWorklet — mic capture, live waveform preview, VU metering, and overdub, as a set of React hooks.
4
4
 
5
5
  ## Features
6
6
 
7
- - **AudioWorklet-based recording** - Low latency, direct PCM access
8
- - **Real-time waveform visualization** - See the waveform as you record
9
- - **React hooks** - Easy integration with React apps
10
- - **Device selection** - Choose from available microphone inputs
11
- - **Optional package** - Only include if you need recording
7
+ - **AudioWorklet-based capture** direct PCM access on the audio thread, no `ScriptProcessorNode`
8
+ - **Live waveform preview** incremental per-channel peaks as you record, before the final `AudioBuffer` exists
9
+ - **Sample-accurate VU metering** a dedicated meter worklet measures peak/RMS on every sample, not just once per animation frame
10
+ - **Multi-channel recording** — auto-detects the microphone's actual channel count from the `MediaStream`
11
+ - **Overdub with latency compensation** record against existing playback; the finalized clip's timeline position accounts for output latency and Tone.js scheduling lookahead, with an optional manual override
12
+ - **Device selection & hot-plug** — enumerate microphones, switch devices between takes (refused while actively recording — a mid-recording switch would silently record silence), auto-fallback if the active device disconnects
13
+ - **Pause/resume** — pauses the worklet itself, not just the UI
14
+ - **Hooks only** — no bundled UI components; wire the state into your own controls or `@waveform-playlist/ui-components`'s `SegmentedVUMeter`
12
15
 
13
16
  ## Installation
14
17
 
@@ -16,208 +19,219 @@ Audio recording support for waveform-playlist using AudioWorklet.
16
19
  npm install @waveform-playlist/recording
17
20
  ```
18
21
 
22
+ `@waveform-playlist/recording` requires these peer dependencies:
23
+
24
+ | Package | Purpose |
25
+ |---------|---------|
26
+ | `react` | ^18.0.0 |
27
+ | `styled-components` | ^6.0.0 — required transitively by `@waveform-playlist/ui-components` |
28
+ | `tone` | ^15.0.0 — recording shares the global AudioContext with `@waveform-playlist/playout` |
29
+
30
+ Pairs with `@waveform-playlist/browser` — see the [Recording guide](https://naomiaro.github.io/waveform-playlist/docs/react/guides/recording) for a full `WaveformPlaylistProvider` integration.
31
+
19
32
  ## Usage
20
33
 
21
34
  ### Basic Recording
22
35
 
23
- ```typescript
24
- import { useRecording, useMicrophoneAccess, RecordButton } from '@waveform-playlist/recording';
36
+ ```tsx
37
+ import { useMicrophoneAccess, useRecording } from '@waveform-playlist/recording';
25
38
 
26
- function RecordingApp() {
27
- const { stream, requestAccess } = useMicrophoneAccess();
28
- const { isRecording, startRecording, stopRecording, peaks, duration } = useRecording(stream);
39
+ function RecordButton() {
40
+ const { stream, hasPermission, requestAccess } = useMicrophoneAccess();
41
+ const { isRecording, duration, peaks, startRecording, stopRecording } = useRecording(stream);
29
42
 
30
43
  const handleRecord = async () => {
31
- if (!stream) {
44
+ if (!hasPermission) {
32
45
  await requestAccess();
46
+ return;
33
47
  }
34
48
 
35
49
  if (isRecording) {
36
50
  const audioBuffer = await stopRecording();
37
- // Use the recorded audio buffer
51
+ // audioBuffer is the finalized AudioBuffer — add it to a track, upload it, etc.
38
52
  } else {
39
53
  await startRecording();
40
54
  }
41
55
  };
42
56
 
43
57
  return (
44
- <div>
45
- <RecordButton isRecording={isRecording} onClick={handleRecord} />
46
- <p>Duration: {duration.toFixed(1)}s</p>
47
- {/* Display waveform using peaks */}
48
- </div>
58
+ <button onClick={handleRecord}>
59
+ {isRecording ? `Stop (${duration.toFixed(1)}s)` : 'Record'}
60
+ </button>
49
61
  );
50
62
  }
51
63
  ```
52
64
 
53
- ### With Microphone Selection
54
-
55
- ```typescript
56
- import {
57
- useMicrophoneAccess,
58
- useRecording,
59
- MicrophoneSelector,
60
- RecordButton,
61
- RecordingIndicator,
62
- } from '@waveform-playlist/recording';
63
-
64
- function RecordingApp() {
65
- const { stream, devices, requestAccess } = useMicrophoneAccess();
66
- const { isRecording, duration, startRecording, stopRecording } = useRecording(stream);
67
- const [selectedDevice, setSelectedDevice] = useState<string>();
68
-
69
- const handleDeviceChange = async (deviceId: string) => {
70
- setSelectedDevice(deviceId);
71
- await requestAccess(deviceId);
72
- };
65
+ ### VU Meter
66
+
67
+ `useMicrophoneLevel` drives level monitoring independently of recording — useful for an input-check screen before the user hits record.
68
+
69
+ ```tsx
70
+ import { useMicrophoneAccess, useMicrophoneLevel } from '@waveform-playlist/recording';
71
+ import { SegmentedVUMeter } from '@waveform-playlist/ui-components';
72
+
73
+ function MicMonitor() {
74
+ const { stream, requestAccess } = useMicrophoneAccess();
75
+ const { levels, peakLevels } = useMicrophoneLevel(stream, { channelCount: 2 });
73
76
 
74
77
  return (
75
- <div>
76
- <MicrophoneSelector
77
- devices={devices}
78
- selectedDeviceId={selectedDevice}
79
- onDeviceChange={handleDeviceChange}
80
- />
81
- <RecordButton
82
- isRecording={isRecording}
83
- onClick={isRecording ? stopRecording : startRecording}
84
- />
85
- <RecordingIndicator isRecording={isRecording} duration={duration} />
86
- </div>
78
+ <>
79
+ <button onClick={() => requestAccess()}>Enable Microphone</button>
80
+ <SegmentedVUMeter levels={levels} peakLevels={peakLevels} />
81
+ </>
87
82
  );
88
83
  }
89
84
  ```
90
85
 
91
- ### Integration with WaveformPlaylist
92
-
93
- ```typescript
94
- import { WaveformPlaylistProvider, Waveform } from '@waveform-playlist/browser';
95
- import { useRecording, useMicrophoneAccess } from '@waveform-playlist/recording';
96
-
97
- function RecordingPlaylist() {
98
- const { stream, requestAccess } = useMicrophoneAccess();
99
- const { peaks, audioBuffer, startRecording, stopRecording } = useRecording(stream);
100
- const [tracks, setTracks] = useState([]);
101
-
102
- const handleStopRecording = async () => {
103
- const buffer = await stopRecording();
104
- if (buffer) {
105
- // Add recorded track to playlist
106
- setTracks([
107
- ...tracks,
108
- {
109
- src: buffer,
110
- name: 'Recording',
111
- },
112
- ]);
113
- }
114
- };
86
+ ### Integrated Recording (with a track list)
87
+
88
+ `useIntegratedRecording` combines microphone access, metering, and recording into one hook that appends the finalized clip directly to a `ClipTrack[]` — the same array shape used by `@waveform-playlist/browser`'s `WaveformPlaylistProvider`.
89
+
90
+ ```tsx
91
+ import { useIntegratedRecording } from '@waveform-playlist/recording';
92
+ import type { ClipTrack } from '@waveform-playlist/core';
93
+
94
+ function Recorder({
95
+ tracks,
96
+ setTracks,
97
+ selectedTrackId,
98
+ currentTime,
99
+ }: {
100
+ tracks: ClipTrack[];
101
+ setTracks: (tracks: ClipTrack[]) => void;
102
+ selectedTrackId: string | null;
103
+ currentTime: number;
104
+ }) {
105
+ const {
106
+ isRecording,
107
+ duration,
108
+ levels,
109
+ peakLevels,
110
+ devices,
111
+ selectedDevice,
112
+ requestMicAccess,
113
+ changeDevice,
114
+ startRecording,
115
+ stopRecording,
116
+ recordingPeaks, // live per-channel peaks — feed straight into your waveform preview
117
+ error,
118
+ } = useIntegratedRecording(tracks, setTracks, selectedTrackId, {
119
+ currentTime,
120
+ channelCount: 2,
121
+ });
115
122
 
116
123
  return (
117
- <WaveformPlaylistProvider tracks={tracks}>
118
- <button onClick={requestAccess}>Request Microphone</button>
119
- <button onClick={startRecording}>Record</button>
120
- <button onClick={handleStopRecording}>Stop</button>
121
- <Waveform />
122
- </WaveformPlaylistProvider>
124
+ <div>
125
+ <button onClick={() => requestMicAccess()}>Enable Microphone</button>
126
+ <button onClick={isRecording ? stopRecording : startRecording} disabled={!selectedTrackId}>
127
+ {isRecording ? `Stop (${duration.toFixed(1)}s)` : 'Record'}
128
+ </button>
129
+ {error && <p>{error.message}</p>}
130
+ </div>
123
131
  );
124
132
  }
125
133
  ```
126
134
 
135
+ Stopping adds a new `AudioClip` to `selectedTrackId` at the timeline position captured when recording STARTED (punch-in semantics): the take lands exactly at the playhead and REPLACES any existing clip content it overlaps — partial overlaps are trimmed, fully-covered clips removed, and a clip spanning the take is split in two.
136
+
127
137
  ## API Reference
128
138
 
129
139
  ### Hooks
130
140
 
131
141
  #### `useMicrophoneAccess()`
132
142
 
133
- Manages microphone access and device enumeration.
143
+ Manages microphone permission, device enumeration, and hot-plug detection.
134
144
 
135
- **Returns:**
136
- - `stream: MediaStream | null` - Active microphone stream
137
- - `devices: MicrophoneDevice[]` - Available microphone devices
138
- - `hasPermission: boolean` - Whether microphone permission is granted
139
- - `isLoading: boolean` - Loading state during access request
140
- - `requestAccess: (deviceId?: string) => Promise<void>` - Request microphone access
141
- - `stopStream: () => void` - Stop the microphone stream
142
- - `error: Error | null` - Error state
145
+ **Returns (`UseMicrophoneAccessReturn`):**
146
+ - `stream: MediaStream | null`
147
+ - `devices: MicrophoneDevice[]` `{ deviceId, label, groupId }`
148
+ - `hasPermission: boolean`
149
+ - `isLoading: boolean`
150
+ - `requestAccess: (deviceId?: string, audioConstraints?: MediaTrackConstraints) => Promise<void>`
151
+ - `stopStream: () => void`
152
+ - `error: Error | null`
153
+
154
+ Requested audio constraints default to `echoCancellation: false`, `noiseSuppression: false`, `autoGainControl: false`, `latency: 0` (raw signal, low latency) — pass `audioConstraints` to override.
143
155
 
144
156
  #### `useRecording(stream, options?)`
145
157
 
146
- Main recording hook using AudioWorklet.
158
+ The core AudioWorklet-based recording hook.
147
159
 
148
160
  **Parameters:**
149
- - `stream: MediaStream | null` - Microphone stream from `useMicrophoneAccess`
161
+ - `stream: MediaStream | null`
150
162
  - `options?: RecordingOptions`
151
- - `sampleRate?: number` - Sample rate (defaults to AudioContext rate)
152
- - `channelCount?: number` - Number of channels (default: 1)
153
- - `samplesPerPixel?: number` - Samples per pixel for peaks (default: 1024)
163
+ - `channelCount?: number` fallback used only if the stream doesn't report its own channel count (default: `1`)
164
+ - `samplesPerPixel?: number` peak resolution for the live preview (default: `1024`)
165
+ - `bits?: 8 | 16` peak value bit depth (default: `16`)
166
+
167
+ **Returns (`UseRecordingReturn`):**
168
+ - `isRecording: boolean`, `isPaused: boolean`, `duration: number` (seconds)
169
+ - `peaks: (Int8Array | Int16Array)[]` — one entry per channel, growing live during recording
170
+ - `audioBuffer: AudioBuffer | null` — set after `stopRecording()` resolves
171
+ - `level: number`, `peakLevel: number` — **deprecated** (always `0`); use `useMicrophoneLevel` for metering. Removed in the next major
172
+ - `startRecording: () => Promise<boolean>` — resolves `true` when the capture pipeline actually started (`false`: no stream, already recording, worklet failure); check it before starting synchronized playback
173
+ - `stopRecording: () => Promise<AudioBuffer | null>` — awaits the worklet's final flush before resolving, so the last samples are never dropped
174
+ - `pauseRecording: () => void`, `resumeRecording: () => void` — pause/resume the worklet itself, not just the UI
175
+ - `error: Error | null`
154
176
 
155
- **Returns:**
156
- - `isRecording: boolean` - Whether recording is active
157
- - `isPaused: boolean` - Whether recording is paused
158
- - `duration: number` - Recording duration in seconds
159
- - `peaks: number[]` - Peak data for waveform visualization
160
- - `audioBuffer: AudioBuffer | null` - Final recorded audio buffer
161
- - `startRecording: () => Promise<void>` - Start recording
162
- - `stopRecording: () => Promise<AudioBuffer | null>` - Stop and finalize recording
163
- - `pauseRecording: () => void` - Pause recording
164
- - `resumeRecording: () => void` - Resume recording
165
- - `error: Error | null` - Error state
177
+ #### `useMicrophoneLevel(stream, options?)`
166
178
 
167
- ### Components
179
+ Sample-accurate VU metering via a separate meter AudioWorklet — independent of `useRecording`, so it works before recording starts.
168
180
 
169
- #### `<RecordButton />`
181
+ **Parameters:**
182
+ - `stream: MediaStream | null`
183
+ - `options?: UseMicrophoneLevelOptions`
184
+ - `updateRate?: number` — Hz (default: `60`)
185
+ - `channelCount?: number` (default: `1`)
170
186
 
171
- Button for starting/stopping recording.
187
+ **Returns (`UseMicrophoneLevelReturn`):**
188
+ - `levels: number[]`, `peakLevels: number[]`, `rmsLevels: number[]` — per channel, normalized 0–1
189
+ - `level: number`, `peakLevel: number` — scalar convenience values (max across channels when `channelCount > 1`)
190
+ - `resetPeak: () => void` — clears held peak indicators, e.g. on device switch
191
+ - `error: Error | null`
172
192
 
173
- **Props:**
174
- - `isRecording: boolean` - Recording state
175
- - `onClick: () => void` - Click handler
176
- - `disabled?: boolean` - Disabled state
177
- - `className?: string` - CSS class name
193
+ #### `useIntegratedRecording(tracks, setTracks, selectedTrackId, options?)`
178
194
 
179
- #### `<MicrophoneSelector />`
195
+ Batteries-included hook: wires `useMicrophoneAccess` + `useMicrophoneLevel` + `useRecording` together and appends the finalized recording to `tracks` as a new `AudioClip`.
180
196
 
181
- Dropdown for selecting microphone device.
197
+ **Parameters:**
198
+ - `tracks: ClipTrack[]`, `setTracks: (tracks: ClipTrack[]) => void`, `selectedTrackId: string | null`
199
+ - `options?: IntegratedRecordingOptions`
200
+ - `currentTime?: number` — playback/cursor position; the clip is captured at this position at record *start* (not stop), so overdubbing while transport is running lands the clip correctly
201
+ - `audioConstraints?: MediaTrackConstraints`
202
+ - `channelCount?: number` (default: `1`)
203
+ - `samplesPerPixel?: number` (default: `1024`)
204
+ - `latencyOffset?: number` — seconds; overrides the auto-computed `outputLatency + lookAhead` compensation applied to the clip's start. `0` disables compensation; omit to auto-compute
182
205
 
183
- **Props:**
184
- - `devices: MicrophoneDevice[]` - Available devices
185
- - `selectedDeviceId?: string` - Currently selected device
186
- - `onDeviceChange: (deviceId: string) => void` - Change handler
187
- - `disabled?: boolean` - Disabled state
188
- - `className?: string` - CSS class name
206
+ **Returns (`UseIntegratedRecordingReturn`):** recording state (`isRecording`, `isPaused`, `duration`, `level`, `peakLevel`, `levels`, `peakLevels`, `rmsLevels`), microphone state (`stream`, `devices`, `hasPermission`, `selectedDevice`), controls (`startRecording` — resolves `true` when capture actually started, `stopRecording`, `pauseRecording`, `resumeRecording`, `requestMicAccess`, `changeDevice`), `recordingPeaks` (live per-channel peaks for preview), and a combined `error`.
189
207
 
190
- #### `<RecordingIndicator />`
208
+ **Overdub with `@waveform-playlist/browser`:** call `usePlaylistControls().setRecordingActive(true, trackId)` *before* `play()` — while the session is active the end-of-timeline auto-stop is suppressed (the take can run past existing audio) and the recorded-over track's existing content is transiently muted (punch-in replaces it). Both reset automatically when the recording ends; check `startRecording()`'s boolean and release the session on `false`.
191
209
 
192
- Visual indicator showing recording status and duration.
210
+ ### Types
193
211
 
194
- **Props:**
195
- - `isRecording: boolean` - Recording state
196
- - `isPaused?: boolean` - Paused state
197
- - `duration: number` - Duration in seconds
198
- - `formatTime?: (seconds: number) => string` - Custom time formatter
199
- - `className?: string` - CSS class name
212
+ `RecordingState`, `RecordingData`, `MicrophoneDevice`, `RecordingOptions`, `UseRecordingReturn`, `UseMicrophoneAccessReturn` are all exported from the package root for consumers building their own UI around these hooks.
200
213
 
201
214
  ## Architecture
202
215
 
203
- The recording implementation uses AudioWorklet for low-latency audio capture:
204
-
205
216
  ```
206
217
  getUserMedia → MediaStream
207
218
 
208
- MediaStreamSource (Web Audio)
219
+ MediaStreamSource (shared global AudioContext)
209
220
 
210
- AudioWorklet Processor
211
- (captures raw PCM data)
221
+ AudioWorklet Processors (from @waveform-playlist/worklets)
222
+ - recording-processor: captures raw PCM per channel
223
+ - meter-processor: sample-accurate peak/RMS
212
224
 
213
- Main Thread (React Hook)
214
- - Accumulates audio data
215
- - Generates peaks in real-time
216
- - Updates waveform visualization
225
+ Main Thread (React Hooks)
226
+ - Accumulates audio data per channel
227
+ - Generates live peaks incrementally
228
+ - Updates VU meter state
217
229
 
218
- Final AudioBuffer
230
+ Final AudioBuffer (after stopRecording's stop-handshake)
219
231
  ```
220
232
 
233
+ Each hook creates its own `MediaStreamSource` from the shared context rather than reusing one across hooks — required for Firefox, which throws if source and destination nodes come from different context instances.
234
+
221
235
  ## Browser Support
222
236
 
223
237
  - Chrome 66+
package/dist/index.d.mts CHANGED
@@ -55,9 +55,22 @@ interface UseRecordingReturn {
55
55
  duration: number;
56
56
  peaks: (Int8Array | Int16Array)[];
57
57
  audioBuffer: AudioBuffer | null;
58
+ /**
59
+ * @deprecated Always 0 — VU levels come from `useMicrophoneLevel`
60
+ * (meter-processor worklet), not this hook. Will be removed in the next
61
+ * major version.
62
+ */
58
63
  level: number;
64
+ /**
65
+ * @deprecated Always 0 — use `useMicrophoneLevel`'s `peakLevel` instead.
66
+ * Will be removed in the next major version.
67
+ */
59
68
  peakLevel: number;
60
- startRecording: () => Promise<void>;
69
+ /** Start capturing. Resolves `true` when the capture pipeline actually
70
+ * started, `false` when it could not (no stream, already recording,
71
+ * worklet/module failure, unmounted). Callers that synchronize playback
72
+ * with recording should check the result before starting playback. */
73
+ startRecording: () => Promise<boolean>;
61
74
  stopRecording: () => Promise<AudioBuffer | null>;
62
75
  pauseRecording: () => void;
63
76
  resumeRecording: () => void;
@@ -121,16 +134,19 @@ interface UseMicrophoneLevelReturn {
121
134
  */
122
135
  resetPeak: () => void;
123
136
  /**
124
- * Per-channel peak levels (0-1). Array length matches channelCount.
137
+ * Per-channel peak levels (0-1). Array length matches the metered channel
138
+ * count: the mic's auto-detected channels, with mono mirrored up to the
139
+ * requested `channelCount`. A mic with MORE channels than `channelCount`
140
+ * yields one entry per actual channel.
125
141
  * True peak: max absolute sample value per analysis frame.
126
142
  */
127
143
  levels: number[];
128
144
  /**
129
- * Per-channel held peak levels (0-1). Array length matches channelCount.
145
+ * Per-channel held peak levels (0-1). Same length contract as `levels`.
130
146
  */
131
147
  peakLevels: number[];
132
148
  /**
133
- * Per-channel RMS levels (0-1). Array length matches channelCount.
149
+ * Per-channel RMS levels (0-1). Same length contract as `levels`.
134
150
  * RMS: root mean square of samples per analysis frame.
135
151
  */
136
152
  rmsLevels: number[];
@@ -164,8 +180,10 @@ declare function useMicrophoneLevel(stream: MediaStream | null, options?: UseMic
164
180
 
165
181
  interface IntegratedRecordingOptions {
166
182
  /**
167
- * Current playback/cursor position in seconds
168
- * Recording will start from max(currentTime, lastClipEndTime)
183
+ * Current playback/cursor position in seconds.
184
+ * Punch-in semantics (#579): the recorded clip lands exactly at this
185
+ * position and REPLACES any existing clip content it overlaps — partial
186
+ * overlaps are trimmed, fully-covered clips removed, spanning clips split.
169
187
  */
170
188
  currentTime?: number;
171
189
  /**
@@ -199,18 +217,25 @@ interface UseIntegratedRecordingReturn {
199
217
  duration: number;
200
218
  level: number;
201
219
  peakLevel: number;
202
- /** Per-channel peak levels (0-1). Array length matches channelCount. */
220
+ /**
221
+ * Per-channel peak levels (0-1). Array length matches the metered channel
222
+ * count (auto-detected mic channels; mono mirrored up to channelCount —
223
+ * see UseMicrophoneLevelReturn.levels).
224
+ */
203
225
  levels: number[];
204
- /** Per-channel held peak levels (0-1). Array length matches channelCount. */
226
+ /** Per-channel held peak levels (0-1). Same length contract as `levels`. */
205
227
  peakLevels: number[];
206
- /** Per-channel RMS levels (0-1). Array length matches channelCount. */
228
+ /** Per-channel RMS levels (0-1). Same length contract as `levels`. */
207
229
  rmsLevels: number[];
208
230
  error: Error | null;
209
231
  stream: MediaStream | null;
210
232
  devices: MicrophoneDevice[];
211
233
  hasPermission: boolean;
212
234
  selectedDevice: string | null;
213
- startRecording: () => void;
235
+ /** Start capturing on the selected track. Resolves `true` when the capture
236
+ * pipeline actually started — callers that synchronize playback with
237
+ * recording (overdub) should check this before starting playback. */
238
+ startRecording: () => Promise<boolean>;
214
239
  stopRecording: () => void;
215
240
  pauseRecording: () => void;
216
241
  resumeRecording: () => void;
package/dist/index.d.ts CHANGED
@@ -55,9 +55,22 @@ interface UseRecordingReturn {
55
55
  duration: number;
56
56
  peaks: (Int8Array | Int16Array)[];
57
57
  audioBuffer: AudioBuffer | null;
58
+ /**
59
+ * @deprecated Always 0 — VU levels come from `useMicrophoneLevel`
60
+ * (meter-processor worklet), not this hook. Will be removed in the next
61
+ * major version.
62
+ */
58
63
  level: number;
64
+ /**
65
+ * @deprecated Always 0 — use `useMicrophoneLevel`'s `peakLevel` instead.
66
+ * Will be removed in the next major version.
67
+ */
59
68
  peakLevel: number;
60
- startRecording: () => Promise<void>;
69
+ /** Start capturing. Resolves `true` when the capture pipeline actually
70
+ * started, `false` when it could not (no stream, already recording,
71
+ * worklet/module failure, unmounted). Callers that synchronize playback
72
+ * with recording should check the result before starting playback. */
73
+ startRecording: () => Promise<boolean>;
61
74
  stopRecording: () => Promise<AudioBuffer | null>;
62
75
  pauseRecording: () => void;
63
76
  resumeRecording: () => void;
@@ -121,16 +134,19 @@ interface UseMicrophoneLevelReturn {
121
134
  */
122
135
  resetPeak: () => void;
123
136
  /**
124
- * Per-channel peak levels (0-1). Array length matches channelCount.
137
+ * Per-channel peak levels (0-1). Array length matches the metered channel
138
+ * count: the mic's auto-detected channels, with mono mirrored up to the
139
+ * requested `channelCount`. A mic with MORE channels than `channelCount`
140
+ * yields one entry per actual channel.
125
141
  * True peak: max absolute sample value per analysis frame.
126
142
  */
127
143
  levels: number[];
128
144
  /**
129
- * Per-channel held peak levels (0-1). Array length matches channelCount.
145
+ * Per-channel held peak levels (0-1). Same length contract as `levels`.
130
146
  */
131
147
  peakLevels: number[];
132
148
  /**
133
- * Per-channel RMS levels (0-1). Array length matches channelCount.
149
+ * Per-channel RMS levels (0-1). Same length contract as `levels`.
134
150
  * RMS: root mean square of samples per analysis frame.
135
151
  */
136
152
  rmsLevels: number[];
@@ -164,8 +180,10 @@ declare function useMicrophoneLevel(stream: MediaStream | null, options?: UseMic
164
180
 
165
181
  interface IntegratedRecordingOptions {
166
182
  /**
167
- * Current playback/cursor position in seconds
168
- * Recording will start from max(currentTime, lastClipEndTime)
183
+ * Current playback/cursor position in seconds.
184
+ * Punch-in semantics (#579): the recorded clip lands exactly at this
185
+ * position and REPLACES any existing clip content it overlaps — partial
186
+ * overlaps are trimmed, fully-covered clips removed, spanning clips split.
169
187
  */
170
188
  currentTime?: number;
171
189
  /**
@@ -199,18 +217,25 @@ interface UseIntegratedRecordingReturn {
199
217
  duration: number;
200
218
  level: number;
201
219
  peakLevel: number;
202
- /** Per-channel peak levels (0-1). Array length matches channelCount. */
220
+ /**
221
+ * Per-channel peak levels (0-1). Array length matches the metered channel
222
+ * count (auto-detected mic channels; mono mirrored up to channelCount —
223
+ * see UseMicrophoneLevelReturn.levels).
224
+ */
203
225
  levels: number[];
204
- /** Per-channel held peak levels (0-1). Array length matches channelCount. */
226
+ /** Per-channel held peak levels (0-1). Same length contract as `levels`. */
205
227
  peakLevels: number[];
206
- /** Per-channel RMS levels (0-1). Array length matches channelCount. */
228
+ /** Per-channel RMS levels (0-1). Same length contract as `levels`. */
207
229
  rmsLevels: number[];
208
230
  error: Error | null;
209
231
  stream: MediaStream | null;
210
232
  devices: MicrophoneDevice[];
211
233
  hasPermission: boolean;
212
234
  selectedDevice: string | null;
213
- startRecording: () => void;
235
+ /** Start capturing on the selected track. Resolves `true` when the capture
236
+ * pipeline actually started — callers that synchronize playback with
237
+ * recording (overdub) should check this before starting playback. */
238
+ startRecording: () => Promise<boolean>;
214
239
  stopRecording: () => void;
215
240
  pauseRecording: () => void;
216
241
  resumeRecording: () => void;