1
1
import { useRef , useEffect , useState , useCallback } from "react" ;
2
2
import audioWorkletUrl from "@/assets/audio-recorder-processor.js?url" ;
3
3
import { api } from "@/trpc/react" ;
4
+ import { Mutex } from "async-mutex" ;
4
5
5
6
// Audio configuration
6
7
const FRAME_SIZE = 512 ; // 32ms at 16kHz
@@ -28,6 +29,7 @@ export const useAudioCapture = ({
28
29
const sourceRef = useRef < MediaStreamAudioSourceNode | null > ( null ) ;
29
30
const workletNodeRef = useRef < AudioWorkletNode | null > ( null ) ;
30
31
const streamRef = useRef < MediaStream | null > ( null ) ;
32
+ const mutexRef = useRef ( new Mutex ( ) ) ;
31
33
32
34
// Subscribe to voice detection updates via tRPC
33
35
api . recording . voiceDetectionUpdates . useSubscription ( undefined , {
@@ -41,117 +43,126 @@ export const useAudioCapture = ({
41
43
} ) ;
42
44
43
45
const startCapture = useCallback ( async ( ) => {
44
- try {
45
- console . log ( "AudioCapture: Starting audio capture" ) ;
46
-
47
- // Get microphone stream
48
- streamRef . current = await navigator . mediaDevices . getUserMedia ( {
49
- audio : {
50
- channelCount : 1 ,
51
- sampleRate : SAMPLE_RATE ,
52
- echoCancellation : true ,
53
- noiseSuppression : true ,
54
- autoGainControl : true ,
55
- } ,
56
- } ) ;
57
-
58
- // Create audio context
59
- audioContextRef . current = new AudioContext ( { sampleRate : SAMPLE_RATE } ) ;
60
-
61
- // Load audio worklet
62
- await audioContextRef . current . audioWorklet . addModule ( audioWorkletUrl ) ;
63
-
64
- // Create nodes
65
- sourceRef . current = audioContextRef . current . createMediaStreamSource (
66
- streamRef . current ,
67
- ) ;
68
- workletNodeRef . current = new AudioWorkletNode (
69
- audioContextRef . current ,
70
- "audio-recorder-processor" ,
71
- ) ;
72
-
73
- // Handle audio frames from worklet
74
- workletNodeRef . current . port . onmessage = async ( event ) => {
75
- if ( event . data . type === "audioFrame" ) {
76
- const frame = event . data . frame ;
77
- console . log ( "AudioCapture: Received frame" , {
78
- frameLength : frame . length ,
79
- isFinal : event . data . isFinal ,
80
- } ) ;
81
- const isFinal = event . data . isFinal || false ;
82
-
83
- // Convert to ArrayBuffer for IPC
84
- const arrayBuffer = frame . buffer . slice (
85
- frame . byteOffset ,
86
- frame . byteOffset + frame . byteLength ,
87
- ) ;
88
-
89
- // Send to main process for VAD processing
90
- // Main process will update voice detection state
91
- await onAudioChunk ( arrayBuffer , 0 , isFinal ) ; // Speech probability will come from main
92
-
93
- console . log (
94
- `AudioCapture: Sent frame: ${ frame . length } samples, isFinal: ${ isFinal } ` ,
95
- ) ;
96
- }
97
- } ;
98
-
99
- // Connect audio graph
100
- sourceRef . current . connect ( workletNodeRef . current ) ;
101
-
102
- console . log ( "AudioCapture: Audio capture started" ) ;
103
- } catch ( error ) {
104
- console . error ( "AudioCapture: Failed to start capture:" , error ) ;
105
- throw error ;
106
- }
46
+ await mutexRef . current . runExclusive ( async ( ) => {
47
+ try {
48
+ console . log ( "AudioCapture: Starting audio capture" ) ;
49
+
50
+ // Get microphone stream
51
+ streamRef . current = await navigator . mediaDevices . getUserMedia ( {
52
+ audio : {
53
+ channelCount : 1 ,
54
+ sampleRate : SAMPLE_RATE ,
55
+ echoCancellation : true ,
56
+ noiseSuppression : true ,
57
+ autoGainControl : true ,
58
+ } ,
59
+ } ) ;
60
+
61
+ // Create audio context
62
+ audioContextRef . current = new AudioContext ( { sampleRate : SAMPLE_RATE } ) ;
63
+
64
+ // Load audio worklet
65
+ await audioContextRef . current . audioWorklet . addModule ( audioWorkletUrl ) ;
66
+
67
+ // Create nodes
68
+ sourceRef . current = audioContextRef . current . createMediaStreamSource (
69
+ streamRef . current ,
70
+ ) ;
71
+ workletNodeRef . current = new AudioWorkletNode (
72
+ audioContextRef . current ,
73
+ "audio-recorder-processor" ,
74
+ ) ;
75
+
76
+ // Handle audio frames from worklet
77
+ workletNodeRef . current . port . onmessage = async ( event ) => {
78
+ if ( event . data . type === "audioFrame" ) {
79
+ const frame = event . data . frame ;
80
+ console . debug ( "AudioCapture: Received frame" , {
81
+ frameLength : frame . length ,
82
+ isFinal : event . data . isFinal ,
83
+ } ) ;
84
+ const isFinal = event . data . isFinal || false ;
85
+
86
+ // Convert to ArrayBuffer for IPC
87
+ const arrayBuffer = frame . buffer . slice (
88
+ frame . byteOffset ,
89
+ frame . byteOffset + frame . byteLength ,
90
+ ) ;
91
+
92
+ // Send to main process for VAD processing
93
+ // Main process will update voice detection state
94
+ await onAudioChunk ( arrayBuffer , 0 , isFinal ) ; // Speech probability will come from main
95
+ }
96
+ } ;
97
+
98
+ // Connect audio graph
99
+ sourceRef . current . connect ( workletNodeRef . current ) ;
100
+
101
+ console . log ( "AudioCapture: Audio capture started" ) ;
102
+ } catch ( error ) {
103
+ console . error ( "AudioCapture: Failed to start capture:" , error ) ;
104
+ throw error ;
105
+ }
106
+ } ) ;
107
107
} , [ onAudioChunk ] ) ;
108
108
109
- const stopCapture = useCallback ( ( ) => {
110
- console . log ( "AudioCapture: Stopping audio capture" ) ;
111
-
112
- // Send flush command to worklet before disconnecting
113
- if ( workletNodeRef . current ) {
114
- workletNodeRef . current . port . postMessage ( { type : "flush" } ) ;
115
- console . log ( "AudioCapture: Sent flush command to worklet" ) ;
116
- }
109
+ const stopCapture = useCallback ( async ( ) => {
110
+ await mutexRef . current . runExclusive ( async ( ) => {
111
+ try {
112
+ console . log ( "AudioCapture: Stopping audio capture" ) ;
117
113
118
- // Disconnect nodes
119
- if ( sourceRef . current && workletNodeRef . current ) {
120
- sourceRef . current . disconnect ( workletNodeRef . current ) ;
121
- }
114
+ // Send flush command to worklet before disconnecting
115
+ if ( workletNodeRef . current ) {
116
+ workletNodeRef . current . port . postMessage ( { type : "flush" } ) ;
117
+ console . log ( "AudioCapture: Sent flush command to worklet" ) ;
118
+ }
122
119
123
- // Close audio context
124
- if ( audioContextRef . current && audioContextRef . current . state !== "closed" ) {
125
- audioContextRef . current . close ( ) ;
126
- }
120
+ // Disconnect nodes
121
+ if ( sourceRef . current && workletNodeRef . current ) {
122
+ sourceRef . current . disconnect ( workletNodeRef . current ) ;
123
+ }
127
124
128
- // Stop media stream
129
- if ( streamRef . current ) {
130
- streamRef . current . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
131
- }
125
+ // Close audio context
126
+ if (
127
+ audioContextRef . current &&
128
+ audioContextRef . current . state !== "closed"
129
+ ) {
130
+ await audioContextRef . current . close ( ) ;
131
+ }
132
132
133
- // Clear refs
134
- audioContextRef . current = null ;
135
- sourceRef . current = null ;
136
- workletNodeRef . current = null ;
137
- streamRef . current = null ;
133
+ // Stop media stream
134
+ if ( streamRef . current ) {
135
+ streamRef . current . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
136
+ }
138
137
139
- setVoiceDetected ( false ) ;
140
- console . log ( "AudioCapture: Audio capture stopped" ) ;
138
+ // Clear refs
139
+ audioContextRef . current = null ;
140
+ sourceRef . current = null ;
141
+ workletNodeRef . current = null ;
142
+ streamRef . current = null ;
143
+
144
+ console . log ( "AudioCapture: Audio capture stopped" ) ;
145
+ } catch ( error ) {
146
+ console . error ( "AudioCapture: Error during stop:" , error ) ;
147
+ throw error ;
148
+ }
149
+ } ) ;
141
150
} , [ ] ) ;
142
151
143
152
// Start/stop based on enabled state
144
153
useEffect ( ( ) => {
145
- if ( enabled ) {
146
- startCapture ( ) . catch ( ( error ) => {
147
- console . error ( "AudioCapture: Failed to start:" , error ) ;
148
- } ) ;
149
- } else {
150
- stopCapture ( ) ;
154
+ if ( ! enabled ) {
155
+ return ;
151
156
}
152
157
158
+ startCapture ( ) . catch ( ( error ) => {
159
+ console . error ( "AudioCapture: Failed to start:" , error ) ;
160
+ } ) ;
161
+
153
162
return ( ) => {
154
- stopCapture ( ) ;
163
+ stopCapture ( ) . catch ( ( error ) => {
164
+ console . error ( "AudioCapture: Failed to stop:" , error ) ;
165
+ } ) ;
155
166
} ;
156
167
} , [ enabled , startCapture , stopCapture ] ) ;
157
168
0 commit comments