1414
1515import { createLogger } from '$lib/utils/logger' ;
1616import type { ProcessedAudio } from './audioProcessor' ;
17+ import { captureOnset } from './onsetDetector' ;
1718
1819const logger = createLogger ( 'AudioRecorder' ) ;
1920
@@ -29,17 +30,43 @@ export interface RecordingOptions {
2930 deviceId ?: string ;
3031
3132 /**
32- * Optional threshold level (0-1) for automatic recording trigger.
33- * Not implemented yet, reserved for future use.
33+ * Optional threshold level (0-1) for automatic onset-triggered recording.
34+ * When provided, recording will arm and wait until the level crosses this threshold,
35+ * then capture exactly 1 second including optional pre-roll.
3436 */
3537 threshold ?: number ;
38+
39+ /**
40+ * Optional pre-roll time in milliseconds included before the trigger point.
41+ * Defaults to 120ms. The post-trigger capture length is reduced accordingly
42+ * so the total captured length remains 1 second.
43+ */
44+ preRollMs ?: number ;
45+
46+ /**
47+ * Minimum time in milliseconds the signal must stay above threshold to trigger.
48+ * Defaults to 12ms.
49+ */
50+ holdMs ?: number ;
51+
52+ /**
53+ * Timeout in milliseconds while waiting for a trigger before aborting.
54+ * Defaults to 10000ms.
55+ */
56+ timeoutMs ?: number ;
57+
58+ /**
59+ * Optional high-pass filter cutoff in Hz to reduce low-frequency rumble.
60+ * Defaults to 80Hz.
61+ */
62+ highpassHz ?: number ;
3663}
3764
3865export interface RecordingProgress {
3966 /**
40- * Recording stage: 'requesting' | 'recording' | 'processing'
67+ * Recording stage: 'requesting' | 'waiting' | ' recording' | 'processing'
4168 */
42- stage : 'requesting' | 'recording' | 'processing' ;
69+ stage : 'requesting' | 'waiting' | ' recording' | 'processing' ;
4370
4471 /**
4572 * Progress percentage (0-100)
@@ -82,25 +109,85 @@ export async function recordAudio(
82109 stream = await navigator . mediaDevices . getUserMedia ( constraints ) ;
83110 logger . debug ( 'Microphone access granted' ) ;
84111
85- // Start recording
86- onProgress ?.( { stage : 'recording' , percentage : 0 } ) ;
112+ if ( typeof options . threshold === 'number' ) {
113+ // Onset-triggered recording path (wait for threshold then capture 1s incl. pre-roll)
114+ onProgress ?.( { stage : 'waiting' , percentage : 0 } ) ;
87115
88- const audioBlob = await recordForDuration ( stream , RECORDING_DURATION_MS , onProgress ) ;
89- logger . debug ( `Recorded ${ audioBlob . size } bytes` ) ;
116+ const preRollMs = Math . max ( 0 , Math . floor ( options . preRollMs ?? 120 ) ) ;
117+ const captureMs = Math . max ( 1 , 1000 - preRollMs ) ;
90118
91- // Stop all tracks
92- stream . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
93- stream = null ;
119+ const { samples, sampleRate } = await captureOnset ( stream , {
120+ threshold : options . threshold ,
121+ preRollMs,
122+ holdMs : Math . max ( 1 , Math . floor ( options . holdMs ?? 12 ) ) ,
123+ timeoutMs : Math . max ( 1000 , Math . floor ( options . timeoutMs ?? 10000 ) ) ,
124+ highpassHz : options . highpassHz ?? 80 ,
125+ captureMs
126+ } ) ;
127+
128+ // Stop all tracks
129+ stream . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
130+ stream = null ;
131+
132+ // Convert to DRUM-compatible format
133+ onProgress ?.( { stage : 'processing' , percentage : 80 } ) ;
134+
135+ let mono = samples ;
136+
137+ if ( sampleRate !== TARGET_SAMPLE_RATE ) {
138+ mono = resample ( mono , sampleRate , TARGET_SAMPLE_RATE ) ;
139+ }
94140
95- // Process the recorded audio
96- onProgress ?.( { stage : 'processing' , percentage : 90 } ) ;
141+ // Ensure exactly 1 second (keep leading pre-roll)
142+ if ( mono . length > MAX_SAMPLES ) {
143+ mono = mono . slice ( 0 , MAX_SAMPLES ) ;
144+ } else if ( mono . length < MAX_SAMPLES ) {
145+ const padded = new Float32Array ( MAX_SAMPLES ) ;
146+ padded . set ( mono , 0 ) ;
147+ mono = padded ;
148+ }
149+
150+ // Convert to 16-bit PCM
151+ const pcmBuffer = new ArrayBuffer ( mono . length * 2 ) ;
152+ const pcmView = new DataView ( pcmBuffer ) ;
153+ for ( let i = 0 ; i < mono . length ; i ++ ) {
154+ const sample = Math . max ( - 1 , Math . min ( 1 , mono [ i ] ) ) ;
155+ const intSample = Math . round ( sample * 32767 ) ;
156+ pcmView . setInt16 ( i * 2 , intSample , true ) ;
157+ }
158+
159+ const processedAudio : ProcessedAudio = {
160+ pcmData : new Uint8Array ( pcmBuffer ) ,
161+ sampleRate : TARGET_SAMPLE_RATE ,
162+ duration : mono . length / TARGET_SAMPLE_RATE ,
163+ originalFileName : `recording-${ Date . now ( ) } .wav`
164+ } ;
165+
166+ logger . info ( 'Recording completed successfully' ) ;
167+ onProgress ?.( { stage : 'processing' , percentage : 100 } ) ;
168+
169+ return processedAudio ;
170+ } else {
171+ // Start fixed-duration recording path
172+ onProgress ?.( { stage : 'recording' , percentage : 0 } ) ;
97173
98- const processedAudio = await processRecording ( audioBlob ) ;
99- logger . info ( 'Recording completed successfully' ) ;
174+ const audioBlob = await recordForDuration ( stream , RECORDING_DURATION_MS , onProgress ) ;
175+ logger . debug ( `Recorded ${ audioBlob . size } bytes` ) ;
100176
101- onProgress ?.( { stage : 'processing' , percentage : 100 } ) ;
177+ // Stop all tracks
178+ stream . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
179+ stream = null ;
180+
181+ // Process the recorded audio
182+ onProgress ?.( { stage : 'processing' , percentage : 90 } ) ;
183+
184+ const processedAudio = await processRecording ( audioBlob ) ;
185+ logger . info ( 'Recording completed successfully' ) ;
102186
103- return processedAudio ;
187+ onProgress ?.( { stage : 'processing' , percentage : 100 } ) ;
188+
189+ return processedAudio ;
190+ }
104191
105192 } catch ( error ) {
106193 // Cleanup on error
0 commit comments