fix: infer actual system audio clock rate

This commit is contained in:
2026-08-27 13:02:00 +02:00
parent 3a9ad8fe0c
commit 204857cdd9
3 changed files with 139 additions and 9 deletions
+100 -3
View File
@@ -27,6 +27,7 @@ private struct SystemCaptureDiagnostics {
var targetFormat = "unavailable"
var selectedInputSampleRate: Double?
var targetSampleRate: Double?
var inputRateInference = "unavailable"
var firstBufferLayout: String?
var callbackCount: UInt64 = 0
var inputFrameCount: UInt64 = 0
@@ -443,18 +444,67 @@ final class AudioManager: NSObject, ObservableObject {
systemDiagnostics.targetSampleRate = targetFormat.sampleRate
var inputFormat: AVAudioFormat?
var converter: AVAudioConverter?
var pendingBuffer: AVAudioPCMBuffer?
var pendingInputTime: AudioTimeStamp?
var pendingNow: AudioTimeStamp?
try tap.run(on: tapQueue) { [weak self] inNow, inputData, inInputTime, _, _ in
guard let self else { return }
if inputFormat == nil {
guard let callbackFormat = self.inputFormat(
for: inputData,
sampleRate: advertisedInputFormat.sampleRate
),
let currentBuffer = self.copyAudioBuffer(from: inputData, format: callbackFormat) else {
self.systemDiagnostics.discardedCallbackCount += 1
return
}
guard let previousBuffer = pendingBuffer,
let previousInputTime = pendingInputTime else {
pendingBuffer = currentBuffer
pendingInputTime = inInputTime.pointee
pendingNow = inNow.pointee
return
}
let selectedSampleRate = self.effectiveInputSampleRate(
advertisedSampleRate: advertisedInputFormat.sampleRate,
previousFrameLength: previousBuffer.frameLength,
previousTimestamp: previousInputTime,
currentTimestamp: inInputTime.pointee
)
inputFormat = self.inputFormat(
for: inputData,
advertisedFormat: advertisedInputFormat
sampleRate: selectedSampleRate
)
if let inputFormat {
self.systemDiagnostics.selectedInputFormat = self.audioFormatSummary(inputFormat)
self.systemDiagnostics.selectedInputSampleRate = inputFormat.sampleRate
converter = AVAudioConverter(from: inputFormat, to: targetFormat)
}
guard let inputFormat, let converter else { return }
self.processAudioBuffer(
{ self.copyAudioBuffer(from: previousBuffer.audioBufferList, format: inputFormat) },
converter: converter,
targetFormat: targetFormat,
source: .system,
callbackTimestamp: previousInputTime,
ioTimestamp: pendingNow
)
pendingBuffer = nil
pendingInputTime = nil
pendingNow = nil
self.processAudioBuffer(
{ self.copyAudioBuffer(from: currentBuffer.audioBufferList, format: inputFormat) },
converter: converter,
targetFormat: targetFormat,
source: .system,
callbackTimestamp: inInputTime.pointee,
ioTimestamp: inNow.pointee
)
return
}
guard let inputFormat, let converter else { return }
// The tap queue is serial. Reusing the converter preserves its
@@ -475,7 +525,7 @@ final class AudioManager: NSObject, ObservableObject {
private func inputFormat(
for inputData: UnsafePointer<AudioBufferList>,
advertisedFormat: AVAudioFormat
sampleRate: Double
) -> AVAudioFormat? {
let buffers = UnsafeMutableAudioBufferListPointer(
UnsafeMutablePointer(mutating: inputData)
@@ -492,12 +542,58 @@ final class AudioManager: NSObject, ObservableObject {
let isInterleaved = buffers.count == 1 && channelCount > 1
return AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: advertisedFormat.sampleRate,
sampleRate: sampleRate,
channels: AVAudioChannelCount(channelCount),
interleaved: isInterleaved
)
}
private func effectiveInputSampleRate(
advertisedSampleRate: Double,
previousFrameLength: AVAudioFrameCount,
previousTimestamp: AudioTimeStamp,
currentTimestamp: AudioTimeStamp
) -> Double {
let timestampsAreValid = previousTimestamp.mFlags.contains(.sampleTimeValid)
&& currentTimestamp.mFlags.contains(.sampleTimeValid)
let sampleTimeDelta = currentTimestamp.mSampleTime - previousTimestamp.mSampleTime
guard timestampsAreValid,
advertisedSampleRate > 0,
previousFrameLength > 0,
sampleTimeDelta.isFinite,
sampleTimeDelta > 0 else {
systemDiagnostics.inputRateInference = "advertised (sample timestamps unavailable)"
return advertisedSampleRate
}
let inferredSampleRate = advertisedSampleRate * Double(previousFrameLength) / sampleTimeDelta
let plausibleRange = (advertisedSampleRate * 0.25)...(advertisedSampleRate * 1.25)
guard inferredSampleRate.isFinite, plausibleRange.contains(inferredSampleRate) else {
systemDiagnostics.inputRateInference = String(
format: "advertised (invalid inference %.3f from %u frames / %.3f sample-time units)",
inferredSampleRate,
previousFrameLength,
sampleTimeDelta
)
return advertisedSampleRate
}
let commonSampleRates: [Double] = [8_000, 11_025, 12_000, 16_000, 22_050, 24_000, 32_000, 44_100, 48_000, 88_200, 96_000]
let selectedSampleRate = commonSampleRates
.min(by: { abs($0 - inferredSampleRate) < abs($1 - inferredSampleRate) })
.flatMap { abs($0 - inferredSampleRate) / $0 <= 0.01 ? $0 : nil }
?? inferredSampleRate
systemDiagnostics.inputRateInference = String(
format: "advertised=%.3f,inferred=%.3f,selected=%.3f,previousFrames=%u,sampleTimeDelta=%.3f",
advertisedSampleRate,
inferredSampleRate,
selectedSampleRate,
previousFrameLength,
sampleTimeDelta
)
return selectedSampleRate
}
private func copyAudioBuffer(
from inputData: UnsafePointer<AudioBufferList>,
format: AVAudioFormat
@@ -863,6 +959,7 @@ final class AudioManager: NSObject, ObservableObject {
"tapAdvertisedFormat=\(systemDiagnostics.tapAdvertisedFormat)",
"aggregateInputFormat=\(systemDiagnostics.aggregateInputFormat)",
"selectedInputFormat=\(systemDiagnostics.selectedInputFormat)",
"inputRateInference=\(systemDiagnostics.inputRateInference)",
"targetFormat=\(systemDiagnostics.targetFormat)",
"firstBufferLayout=\(systemDiagnostics.firstBufferLayout ?? "unavailable")",
"callbackCount=\(systemDiagnostics.callbackCount)",