From 364190a4050478deb0e6357df0a4e5d0661be32e Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Sun, 16 Aug 2026 17:46:18 +0000 Subject: [PATCH] Two fixes the room demanded at once: whistlecultspatial stops withholding — the words sing in the loops from act III on (SPATIAL opens the wordsIn gate at bar 24; the straight render keeps v10.1's bar-76 withholding), because hearing 'run real fast' orbit the machines IS the piece — and the receiver grows a sequential render cursor: the stream position advances sample-exactly per callback instead of being re-derived from the ever-wobbling clock offset (the source of the pops), with a 3 ms deadband, ±1-sample slew to track drift, and a hard resync only past 80 ms --- pop/cult/c/cultremix.c | 8 +++++++- slab/macos-audio/Sources/ACMacAudio/RoomReceiver.swift | 29 ++++++++++++++++++++++++++--- 2 file(s) changed, 33 insertion(s)(+), 4 deletion(s)(-) diff --git a/pop/cult/c/cultremix.c b/pop/cult/c/cultremix.c --- a/pop/cult/c/cultremix.c +++ b/pop/cult/c/cultremix.c @@ -836,7 +836,13 @@ static inline int sosBar(int b) { return b >= 16 && b < 24; } static inline int kickOn(int b) { return !(inS(b, S_CARRIER) || inS(b, S_SECRET) || inS(b, S_CARRIEROFF)); } static inline int hatOn(int b) { return kickOn(b) || inS(b, S_SECRET); } static inline int dense(int b) { return inS(b, S_REPLY) || inS(b, S_WHOLE); } -static inline int wordsIn(int bar) { return bar >= SB[S_WHOLE][0]; } +static inline int wordsIn(int bar) { + // whistlecultspatial: the room version doesn't withhold — "run real + // fast" sings in the loops from act III on, because hearing the words + // orbit between the machines IS the piece. The straight render keeps + // v10.1's withholding-until-bar-76. + return bar >= SB[SPATIAL ? S_MESSAGE : S_WHOLE][0]; +} static int degAt(int bar) { if (inS(bar, S_WHOLE)) return HOME[((bar - SB[S_WHOLE][0]) % 8) / 2]; diff --git a/slab/macos-audio/Sources/ACMacAudio/RoomReceiver.swift b/slab/macos-audio/Sources/ACMacAudio/RoomReceiver.swift --- a/slab/macos-audio/Sources/ACMacAudio/RoomReceiver.swift +++ b/slab/macos-audio/Sources/ACMacAudio/RoomReceiver.swift @@ -19,6 +19,13 @@ private var session: UInt32? private var chunks: [ACBufferedAudio] = [] var onDebug: (@Sendable (String) -> Void)? private var lastDebugNanos: UInt64 = 0 + /// Sequential render cursor (server-time of the next sample to render). + /// The stream position advances sample-exactly per callback, so clock + /// wobble can never shift the read position mid-stream — the audible + /// pops of re-deriving the window from a moving offset each callback. + /// Small clock error is absorbed by a ±1-sample slew, real breaks by a + /// hard resync. + private var cursorNanos: UInt64? func insert(_ packet: ACRoomAudioPacket) { guard packet.sampleRate == UInt32(ACRoomWire.sampleRate), packet.channels == 2 else { return } @@ -50,12 +57,28 @@ if chunks.count > 600 { chunks.removeFirst(chunks.count - 600) } lock.unlock() } - func render(serverStartNanos: UInt64, frameCount: Int, channel: ACRoomWire.Channel, + func render(targetNanos: UInt64, frameCount: Int, channel: ACRoomWire.Channel, gain: Float, leftOutput: UnsafeMutablePointer, rightOutput: UnsafeMutablePointer) { leftOutput.initialize(repeating: 0, count: frameCount) rightOutput.initialize(repeating: 0, count: frameCount) - let renderEnd = serverStartNanos + UInt64(Double(frameCount) * 1_000_000_000 / ACRoomWire.sampleRate) + let sampleNanos = UInt64(1_000_000_000 / ACRoomWire.sampleRate) + let frameNanos = UInt64(Double(frameCount) * 1_000_000_000 / ACRoomWire.sampleRate) + var serverStartNanos = targetNanos + if let cursor = cursorNanos { + let error = Int64(bitPattern: cursor &- targetNanos) + if abs(error) > 80_000_000 { + serverStartNanos = targetNanos // real break: resync once + } else if error > 3_000_000 { + serverStartNanos = cursor &- sampleNanos // slew back, 1 sample + } else if error < -3_000_000 { + serverStartNanos = cursor &+ sampleNanos // slew forward, 1 sample + } else { + serverStartNanos = cursor // steady: seamless + } + } + cursorNanos = serverStartNanos &+ frameNanos + let renderEnd = serverStartNanos + frameNanos lock.lock() // Scheduling truth every ~5 s: where the newest/oldest buffered // chunks sit relative to the render clock. "Signal present" with @@ -225,7 +248,7 @@ localNanos = ACHostClock.nowNanos() } let serverSigned = Int64(clamping: localNanos) + offset guard serverSigned >= 0 else { return noErr } - self.store.render(serverStartNanos: UInt64(serverSigned), frameCount: Int(frames), + self.store.render(targetNanos: UInt64(serverSigned), frameCount: Int(frames), channel: self.configuration.channel, gain: self.configuration.gain, leftOutput: left, rightOutput: right) -- tangled.sh