Skip to content

Commit 32fa75b

Browse files
r3dbarsclaude
andcommitted
Drop logging from system audio I/O proc
The CoreAudio I/O block is real-time critical and was emitting debug, warning, and error logs on the audio callback thread (throttled but still per-callback). Strip all logging, remove the now-dead callbackCount captured var, and document the RT-safety contract so future edits stay on the right side of it. Counter increments still use short NSLock acquisitions (acceptable); cleanup logs aggregate stats off-thread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8fde23a commit 32fa75b

1 file changed

Lines changed: 34 additions & 50 deletions

File tree

‎Sources/TranscriptedCore/Audio/SystemAudioProcessTap.swift‎

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)