From a8f88c5cab13912564cfad158e9b9edd80904800 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Sat, 24 Oct 2026 22:54:12 -0400 Subject: [PATCH] A Drive that fails and rejoins inside one audio block keeps its new join request (#945) The join block read the requests, acted on them, then cleared what it read. A stop and a reattach landing in between had their new request wiped, and the Copy was fed with no join frame. The block now claims the requests with one exchange, so a later request survives to the next block, and a Copy stopped or asked to join again since the claim is taken back out of capturing until that block serves it. Real-time path (ADR 0002): atomics only. Assisted-by: Claude Code:claude-opus-5-5 --- .../Sources/Recording/Capture.swift | 16 +++++++------- .../Tests/RecordingTests/CopyJoinTests.swift | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Packages/ShowRecorderKit/Sources/Recording/Capture.swift b/Packages/ShowRecorderKit/Sources/Recording/Capture.swift index d28a5acb..1c2e1804 100644 --- a/Packages/ShowRecorderKit/Sources/Recording/Capture.swift +++ b/Packages/ShowRecorderKit/Sources/Recording/Capture.swift @@ -89,7 +89,8 @@ final class Capture: Sendable { private let hostClock: @Sendable () -> UInt64 /// A gap shorter than this (frames) is timing jitter, not lost input. private let gapThreshold: Int - /// Tests only: runs on the real-time thread between reading the join requests and acting on them (#544). Nil otherwise. + /// Tests only: runs on the real-time thread after a join block sets the join frames, before the Copies are added to + /// `capturing` (#544, #945). Nil otherwise. private let afterJoinRead: (@Sendable () -> Void)? /// The host clock's ticks per second on this machine. static let machTicksPerSecond: Double = { @@ -220,9 +221,9 @@ final class Capture: Sendable { func receive(_ block: AudioBlock) { meters.record(block) preRoll?.write(block) - let requested = joinRequests.load(ordering: .acquiring) + // A read first, so a block with nothing waiting writes nothing shared; the claim is one exchange (#945). + let requested = joinRequests.load(ordering: .relaxed) == 0 ? 0 : joinRequests.exchange(0, ordering: .acquiringAndReleasing) if requested != 0 { - afterJoinRead?() if preRollStart.load(ordering: .relaxed) == -2 { // The Pre-roll buffer already holds this block, so the live audio starts before it. let start = (preRoll?.totalWrittenFrames ?? block.frameCount) - block.frameCount @@ -235,11 +236,12 @@ final class Capture: Sendable { for copy in 0..