From 7abba26948b5f625d61a7c5c260b92fe15ff8a20 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 16 Aug 2026 12:54:46 -0400 Subject: [PATCH] =?UTF-8?q?The=20room's=20true=20silence=20bug,=20caught?= =?UTF-8?q?=20by=20the=20new=20meters:=20the=20tap=20aggregate=20runs=20on?= =?UTF-8?q?=20the=20OUTPUT=20DEVICE's=20clock=20(96=20kHz=20on=20neo)=20wh?= =?UTF-8?q?ile=20the=20tap=20claims=2048=20kHz,=20so=20captured=20audio=20?= =?UTF-8?q?was=20wrapped=20at=20the=20wrong=20rate=20=E2=80=94=20half=20pi?= =?UTF-8?q?tch=20had=20it=20ever=20played=20=E2=80=94=20and=20the=20presen?= =?UTF-8?q?tation=20clock=20raced=202=C3=97=20real=20time,=20scheduling=20?= =?UTF-8?q?every=20chunk=20for=20a=20future=20the=20render=20window=20neve?= =?UTF-8?q?r=20reaches=20(receiver=20windows=20showed=20+1=20s=20drifting?= =?UTF-8?q?=20to=20+16=20s=20in=2015=20s=20while=20both=20ends=20read=20's?= =?UTF-8?q?ignal=20present').=20The=20tap=20now=20trusts=20the=20aggregate?= =?UTF-8?q?'s=20nominal=20rate=20over=20its=20own=20claim,=20and=20the=20s?= =?UTF-8?q?ender=20resyncs=20its=20presentation=20clock=20with=20a=20fresh?= =?UTF-8?q?=20session=20if=20it=20ever=20drifts=20a=20second=20from=20wall?= =?UTF-8?q?-clock=20+=20latency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/ACMacAudio/ProcessAudioTap.swift | 18 ++++++++++++++++++ .../Sources/ACMacAudio/RoomSender.swift | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/slab/macos-audio/Sources/ACMacAudio/ProcessAudioTap.swift b/slab/macos-audio/Sources/ACMacAudio/ProcessAudioTap.swift index 703f4a9792..0ecbbf3413 100644 --- a/slab/macos-audio/Sources/ACMacAudio/ProcessAudioTap.swift +++ b/slab/macos-audio/Sources/ACMacAudio/ProcessAudioTap.swift @@ -77,6 +77,24 @@ public final class ACProcessAudioTap: @unchecked Sendable { ] status = AudioHardwareCreateAggregateDevice(aggregateDescription as CFDictionary, &aggregateID) guard status == noErr else { throw ACAudioRoomError.coreAudio("Create tap aggregate", status) } + // The IOProc runs on the AGGREGATE's clock (its main subdevice is + // the current output device) and drift compensation delivers the + // tap's samples AT THAT RATE. On a 96 kHz interface, a tap that + // claims 48 kHz would otherwise have its buffers wrapped and + // paced as 48 kHz: audio at the wrong pitch and a presentation + // clock racing 2× real time, so receivers chase chunks scheduled + // for a future that never arrives. Trust the aggregate's rate. + if let aggregateRate = try? Self.read(aggregateID, + selector: kAudioDevicePropertyNominalSampleRate, + defaultValue: Double(0)), + aggregateRate > 0, abs(aggregateRate - format.sampleRate) > 1 { + var corrected = stream + corrected.mSampleRate = aggregateRate + if let correctedFormat = AVAudioFormat(streamDescription: &corrected) { + self.format = correctedFormat + onLog?("tap delivers at aggregate rate \(Int(aggregateRate)) Hz (tap claimed \(Int(format.sampleRate)))") + } + } self.handler = handler status = AudioDeviceCreateIOProcIDWithBlock(&ioProcID, aggregateID, queue) { [weak self] _, inputData, _, _, _ in diff --git a/slab/macos-audio/Sources/ACMacAudio/RoomSender.swift b/slab/macos-audio/Sources/ACMacAudio/RoomSender.swift index bfd15bdf1b..e211bffadb 100644 --- a/slab/macos-audio/Sources/ACMacAudio/RoomSender.swift +++ b/slab/macos-audio/Sources/ACMacAudio/RoomSender.swift @@ -173,6 +173,19 @@ public final class ACAudioRoomSender: @unchecked Sendable { } lastAudioNanos = now + // Insurance against pacing bugs (a wrong input rate, device quirks): + // if the presentation clock drifts more than a second from + // wall-clock + latency, resync with a fresh session rather than + // keep scheduling audio for a time that never comes. + let target = now + configuration.latencyMilliseconds * 1_000_000 + let drift = Int64(bitPattern: nextPresentationNanos &- target) + if abs(drift) > 1_000_000_000 { + log(String(format: "presentation clock drifted %+.1f s — resyncing", Double(drift) / 1e9)) + session = UInt32.random(in: 1...UInt32.max) + sequence = 0 + nextPresentationNanos = target + } + var offset = 0 let total = Int(output.frameLength) while offset < total { -- 2.51.2