@@ -91,64 +91,48 @@ extension SystemAudioCapture {
9191 throw " Failed to create AVAudioFormat from tap description "
9292 }
9393
94- // Track callback count for debugging (thread-safe via atomic-like access)
95- var callbackCount = 0
96-
97- // Create I/O proc to receive audio buffers
94+ // Create I/O proc to receive audio buffers.
95+ //
96+ // CRITICAL: this closure runs on the CoreAudio I/O thread. It must be
97+ // real-time safe — NO logging, NO allocations beyond AVAudioPCMBuffer
98+ // wrapper construction, NO blocking work. Counter increments use short
99+ // NSLock acquisitions which are acceptable here; anything heavier must
100+ // be deferred. cleanup()/logStats() surface counters off the RT thread.
98101 var err = AudioDeviceCreateIOProcIDWithBlock ( & deviceProcID, aggregateDeviceID, queue) { [ weak self] inNow, inInputData, inInputTime, outOutputData, inOutputTime in
99102 guard let self = self , let bufferCallback = self . bufferCallback else {
100- AppLogger . audioSystem. warning ( " I/O Proc: self or bufferCallback is nil " )
101103 return
102104 }
103105
104- callbackCount += 1
105- if callbackCount <= 3 {
106- AppLogger . audioSystem. debug ( " I/O Proc callback " , [ " count " : " \( callbackCount) " ] )
106+ guard let buffer = AVAudioPCMBuffer ( pcmFormat: format, bufferListNoCopy: inInputData, deallocator: nil ) else {
107+ return
107108 }
108109
109- do {
110- guard let buffer = AVAudioPCMBuffer ( pcmFormat: format, bufferListNoCopy: inInputData, deallocator: nil ) else {
111- AppLogger . audioSystem. error ( " I/O Proc: Failed to create PCM buffer " )
112- throw " Failed to create PCM buffer "
113- }
114-
115- // Track total buffers received
116- self . incrementStats ( hasData: false )
117-
118- // CRITICAL FIX: Check for zero-frame buffers BEFORE updating watchdog
119- // Zero-frame buffers were defeating the watchdog by updating lastBufferTime
120- // even though no actual audio was being captured (device switch scenario)
121- let frameLength = buffer. frameLength
122- if frameLength == 0 {
123- // Log throttled: first 10, then every 100th
124- if callbackCount <= 10 || callbackCount % 100 == 0 {
125- AppLogger . audioSystem. warning ( " Zero-frame buffer " , [ " callbackCount " : " \( callbackCount) " ] )
126- }
127- self . incrementDropped ( )
128- // Do NOT update lastBufferTime - let watchdog detect silence
129- return
130- }
131-
132- if callbackCount <= 3 {
133- AppLogger . audioSystem. debug ( " Buffer created " , [ " frames " : " \( frameLength) " ] )
134- }
135-
136- // Only update watchdog timestamp for buffers with actual data
137- // CACurrentMediaTime() is allocation-free and safe for real-time threads
138- self . lastBufferTime = CACurrentMediaTime ( )
139- if !self . hasReceivedFirstBuffer {
140- self . hasReceivedFirstBuffer = true
141- }
142- if self . recoveryAttempts > 0 {
143- self . recoveryAttempts = 0
144- }
145- self . markBufferHasData ( )
146-
147- // Send buffer to callback
148- bufferCallback ( buffer)
149- } catch {
150- AppLogger . audioSystem. error ( " I/O Proc error " , [ " error " : " \( error) " ] )
110+ // Track total buffers received
111+ self . incrementStats ( hasData: false )
112+
113+ // CRITICAL FIX: Check for zero-frame buffers BEFORE updating watchdog.
114+ // Zero-frame buffers were defeating the watchdog by updating lastBufferTime
115+ // even though no actual audio was being captured (device switch scenario).
116+ let frameLength = buffer. frameLength
117+ if frameLength == 0 {
118+ self . incrementDropped ( )
119+ // Do NOT update lastBufferTime — let watchdog detect silence
120+ return
151121 }
122+
123+ // Only update watchdog timestamp for buffers with actual data.
124+ // CACurrentMediaTime() is allocation-free and safe for real-time threads.
125+ self . lastBufferTime = CACurrentMediaTime ( )
126+ if !self . hasReceivedFirstBuffer {
127+ self . hasReceivedFirstBuffer = true
128+ }
129+ if self . recoveryAttempts > 0 {
130+ self . recoveryAttempts = 0
131+ }
132+ self . markBufferHasData ( )
133+
134+ // Send buffer to callback
135+ bufferCallback ( buffer)
152136 }
153137
154138 guard err == noErr else {
0 commit comments