From 1b7c41f1bd13322dc1930b7a7d6dcc7eead50fe4 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 07:46:34 -0600 Subject: [PATCH 01/20] Improve accuracy of frames rendered bookkeeping --- Package.resolved | 9 + Package.swift | 2 + Sources/CSFBAudioEngine/Player/AudioPlayer.h | 42 ++- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 343 ++++++++++-------- 4 files changed, 244 insertions(+), 152 deletions(-) diff --git a/Package.resolved b/Package.resolved index b5d9e2fc..9bea48cd 100644 --- a/Package.resolved +++ b/Package.resolved @@ -63,6 +63,15 @@ "version" : "12.13.0" } }, + { + "identity" : "cxxringbuffer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/CXXRingBuffer", + "state" : { + "revision" : "8d859aa73d115273afcebe754b22bf50d3ea0621", + "version" : "0.6.1" + } + }, { "identity" : "cxxtaglib", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 8896cb02..9f883179 100644 --- a/Package.swift +++ b/Package.swift @@ -28,6 +28,7 @@ let package = Package( .package(url: "https://github.com/sbooth/CXXAudioRingBuffer", .upToNextMinor(from: "0.1.1")), .package(url: "https://github.com/sbooth/CXXDispatchSemaphore", .upToNextMinor(from: "0.4.1")), .package(url: "https://github.com/sbooth/CXXMessageQueue", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/sbooth/CXXRingBuffer", .upToNextMinor(from: "0.6.1")), .package(url: "https://github.com/sbooth/CXXUnfairLock", .upToNextMinor(from: "0.3.1")), // Standalone dependencies from source @@ -66,6 +67,7 @@ let package = Package( .product(name: "CXXAudioRingBuffer", package: "CXXAudioRingBuffer"), .product(name: "CXXDispatchSemaphore", package: "CXXDispatchSemaphore"), .product(name: "CXXMessageQueue", package: "CXXMessageQueue"), + .product(name: "CXXRingBuffer", package: "CXXRingBuffer"), .product(name: "CXXUnfairLock", package: "CXXUnfairLock"), // Standalone dependencies .product(name: "dumb", package: "CDUMB"), diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index dba628d6..8915b488 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -15,6 +15,7 @@ #import #import #import +#import #import @@ -34,6 +35,35 @@ namespace sfb { +namespace detail { + +/// A descriptor for a decoded chunk of audio. +struct DecodedChunkDescriptor final { + /// The playback generation at the time this chunk was decoded + uint64_t playbackGeneration_{0}; + /// Decoder sequence number that produced the audio. + uint64_t sequenceNumber_{0}; + /// Decoder frame position for the first audio frame in the chunk. + int64_t framePosition_{0}; + /// Number of audio frames in the chunk. + uint32_t frameLength_{0}; +}; + +/// A descriptor for a rendering chunk of audio. +struct RenderingChunkDescriptor final { + /// The decoded chunk descriptor. + DecodedChunkDescriptor descriptor_{}; + /// The number of frames consumed from `descriptor_` + uint32_t framesConsumed_{0}; + + /// Returns the number of frames remaining in this chunk + [[nodiscard]] uint32_t framesRemaining() const noexcept { return descriptor_.frameLength_ - framesConsumed_; } + /// Returns the frame position of the next frame in this chunk + [[nodiscard]] int64_t framePosition() const noexcept { return descriptor_.framePosition_ + framesConsumed_; } +}; + +} /* namespace detail */ + // MARK: - AudioPlayer /// SFBAudioPlayer implementation @@ -54,7 +84,12 @@ class AudioPlayer final { using DecoderStateVector = std::vector>; /// Ring buffer transferring audio between the decoding thread and the render block - spsc::AudioRingBuffer audioRingBuffer_; + spsc::AudioRingBuffer audioBuffer_; + /// Ring buffer transferring audio metadata between the decoding thread and the render block + spsc::RingBuffer audioMetadata_; + /// The current transport epoch + std::atomic playbackGeneration_{1}; + static_assert(std::atomic::is_always_lock_free, "Lock-free std::atomic required"); /// Active decoders and associated state DecoderStateVector activeDecoders_; @@ -249,6 +284,8 @@ class AudioPlayer final { /// Render block implementation OSStatus render(BOOL &isSilence, const AudioTimeStamp ×tamp, AVAudioFrameCount frameCount, AudioBufferList *_Nonnull outputData) noexcept; + /// The current rendering chunk descriptor + detail::RenderingChunkDescriptor renderingChunk_{}; // MARK: - Events @@ -311,6 +348,9 @@ class AudioPlayer final { /// Returns the first decoder state in `activeDecoders_` that has not been canceled DecoderState *_Nullable firstActiveDecoderState() const noexcept; + /// Returns the decoder state in `activeDecoders_` with the specified sequence number + DecoderState *_Nullable decoderStateWithSequenceNumber(uint64_t sequenceNumber) const noexcept; + public: // MARK: - AVAudioEngine Notification Handling diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index b7e97583..7bb0a969 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -25,13 +25,16 @@ #import #import #import +#import #import namespace { -/// The default ring buffer capacity -constexpr std::size_t ringBufferCapacity = 16384; -/// The minimum number of frames to write to the ring buffer +/// The default audio ring buffer capacity in frames +constexpr std::size_t audioBufferCapacity = 16384; +/// The default audio metadata buffer capacity in bytes +constexpr std::size_t metadataBufferCapacity = 2048; +/// The minimum number of frames to write to the audio ring buffer constexpr AVAudioFrameCount ringBufferChunkSize = 2048; /// The number of nanoseconds in one second @@ -273,7 +276,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re /// Returns `true` if a seek is pending bool isSeekRequested() const noexcept; /// Performs the pending seek request - bool performSeek(NSError **error) noexcept; + std::optional performSeek(NSError **error) noexcept; }; std::atomic AudioPlayer::DecoderState::sequenceCounter_{1}; @@ -422,7 +425,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } /// Performs the pending seek request -inline bool AudioPlayer::DecoderState::performSeek(NSError **error) noexcept { +inline std::optional AudioPlayer::DecoderState::performSeek(NSError **error) noexcept { const auto requestedFrame = requestedFrame_.load(std::memory_order_acquire); #if DEBUG assert(requestedFrame != SFBUnknownFramePosition); @@ -443,28 +446,25 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re *error = seekError; } clearSeekRequest(); - return false; + return std::nullopt; } // Reset the converter to flush any buffers [converter_ reset]; - const auto framePosition = decoder_.framePosition; - if (framePosition != SFBUnknownFramePosition) { - if (framePosition != requestedFrame) { - os_log_info(log_, "Inaccurate seek to frame %lld, got %lld", requestedFrame, framePosition); - } - - // Update the frame counters accordingly - // A seek is handled in essentially the same way as initial playback - framesDecoded_.store(framePosition, std::memory_order_release); - framesRendered_.store(framePosition, std::memory_order_release); - } - // Clear the seek request clearSeekRequest(); - return framePosition != SFBUnknownFramePosition; + const auto framePosition = decoder_.framePosition; + if (framePosition == SFBUnknownFramePosition) { + os_log_error(log_, "Unknown frame position in %{public}@ after seeking to frame %lld", decoder_, requestedFrame); + return std::nullopt; + } + if (framePosition != requestedFrame) { + os_log_info(log_, "Inaccurate seek to frame %lld, got %lld", requestedFrame, framePosition); + } + + return framePosition; } } /* namespace sfb */ @@ -482,19 +482,26 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re throw std::runtime_error("Unable to create AVAudioFormat"); } - // Allocate the audio ring buffer moving audio from the decoder queue to the render block - if (!audioRingBuffer_.allocate(*(format.streamDescription), ringBufferCapacity)) { + // Allocate the audio buffer carrying audio from the decoder thread to the render block + if (!audioBuffer_.allocate(*(format.streamDescription), audioBufferCapacity)) { os_log_error(log_, - "Unable to create audio ring buffer: spsc::AudioRingBuffer::allocate failed with format " + "Unable to create audio buffer: spsc::AudioRingBuffer::allocate failed with format " "%{public}@ and capacity %zu", - SFBASBDFormatDescription(format.streamDescription), ringBufferCapacity); + SFBASBDFormatDescription(format.streamDescription), audioBufferCapacity); throw std::runtime_error("spsc::AudioRingBuffer::allocate failed"); } + // Allocate the metadata buffer carrying decoded chunk descriptors from the decoder thread to the render block + if (!audioMetadata_.allocate(metadataBufferCapacity)) { + os_log_error(log_, "Unable to create metadata buffer: spsc::RingBuffer::allocate failed with capacity %zu", + metadataBufferCapacity); + throw std::runtime_error("spsc::RingBuffer::allocate failed"); + } + // ======================================== // Event Processing Setup - // Create the dispatch queue used for event processing + // Create the dispatch queue used for asynchronous event processing auto attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); if (attr == nullptr) { os_log_error(log_, "dispatch_queue_attr_make_with_qos_class failed"); @@ -1172,7 +1179,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re while (!stoken.stop_requested()) { // The decoder state being processed DecoderState *decoderState = nullptr; - auto ringBufferStale = false; { std::lock_guard lock{activeDecodersMutex_}; @@ -1180,6 +1186,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Process cancellations auto signal = false; auto anyCanceled = false; + for (const auto &decoderState : activeDecoders_) { const auto flags = decoderState->loadFlags(); if (bits::is_set_or_is_clear(flags, DecoderState::Flags::isCanceled, @@ -1193,9 +1200,12 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re os_log_error(log_, "Aborting decoding for %{public}@ due to error", decoderState->decoder_); } - // Drain the ring buffer if the decoder could have contributed any stale frames if (bits::is_set(flags, DecoderState::Flags::decodingStarted)) { - ringBufferStale = true; + // Drain the ring buffer since the decoder could have contributed stale frames + setFlags(Flags::drainRequired); + + // Increment the playback epoch to expire any inflight events + playbackGeneration_.fetch_add(1, std::memory_order_acq_rel); } decoderState->setFlags(DecoderState::Flags::isCanceled); @@ -1225,24 +1235,27 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Process pending seeks if (decoderState != nullptr && decoderState->isSeekRequested()) { - // Mute until the seek is complete and the ring buffer is refilled - setFlags(Flags::isMuted); - - if (NSError *seekError = nil; !decoderState->performSeek(&seekError)) { + NSError *seekError = nil; + const auto framePosition = decoderState->performSeek(&seekError); + if (!framePosition.has_value()) { decoderState->error_ = seekError; decoderState->setFlags(DecoderState::Flags::cancelRequested); continue; } - if (const auto frame = decoderState->framesDecoded_.load(std::memory_order_acquire); - events_.enqueue(EventCommand::seek, decoderState->sequenceNumber_, frame)) { + // Mute until the seek is complete and the ring buffer is drained and refilled + setFlags(Flags::isMuted | Flags::drainRequired); + + // Increment the playback epoch to expire any inflight events + playbackGeneration_.fetch_add(1, std::memory_order_acq_rel); + + decoderState->framesDecoded_.store(framePosition.value(), std::memory_order_release); + if (events_.enqueue(EventCommand::seek, decoderState->sequenceNumber_, framePosition.value())) { eventSemaphore_.signal(); } else { os_log_fault(log_, "Error writing decoder seek event"); } - ringBufferStale = true; - if (bits::is_set(decoderState->loadFlags(), DecoderState::Flags::decodingComplete)) { os_log_debug(log_, "Resuming decoding for %{public}@", decoderState->decoder_); @@ -1272,17 +1285,28 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (bits::is_set(flags, DecoderState::Flags::isCanceled)) { continue; } + if (bits::is_set(flags, DecoderState::Flags::decodingStarted)) { os_log_debug(log_, "Suspending decoding for %{public}@", nextDecoderState->decoder_); // TODO: Investigate a per-state buffer to mitigate frame loss if (nextDecoderState->decoder_.supportsSeeking) { nextDecoderState->requestSeekToFrame(0); - if (NSError *seekError = nil; !nextDecoderState->performSeek(&seekError)) { + + NSError *seekError = nil; + const auto framePosition = nextDecoderState->performSeek(&seekError); + if (!framePosition.has_value()) { nextDecoderState->error_ = seekError; nextDecoderState->setFlags(DecoderState::Flags::cancelRequested); continue; } + + nextDecoderState->framesDecoded_.store(framePosition.value(), std::memory_order_release); + if (events_.enqueue(EventCommand::seek, nextDecoderState->sequenceNumber_, framePosition.value())) { + eventSemaphore_.signal(); + } else { + os_log_fault(log_, "Error writing decoder seek event"); + } } else { os_log_error(log_, "Discarding %lld frames from %{public}@", nextDecoderState->framesDecoded_.load(std::memory_order_acquire), @@ -1302,12 +1326,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } } - // Request a drain of the ring buffer during the next render cycle to prevent audible artifacts from seeking or - // cancellation - if (ringBufferStale) { - setFlags(Flags::drainRequired); - } - // Get the earliest decoder state that has not completed decoding { std::lock_guard lock{activeDecodersMutex_}; @@ -1430,7 +1448,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re }(); if (okToReconfigure) { - clearFlags(Flags::drainRequired); + // clearFlags(Flags::drainRequired); formatMismatch = false; os_log_debug(log_, "Non-gapless join for %{public}@", decoderState->decoder_); @@ -1466,12 +1484,19 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (decoderState != nullptr) { if (const auto flags = loadFlags(); bits::is_clear(flags, Flags::drainRequired)) { - // Decode and write chunks to the ring buffer - while (audioRingBuffer_.freeSpace() >= ringBufferChunkSize) { + // Decode and write chunks and metadata to the ring buffers + while (audioBuffer_.freeSpace() >= ringBufferChunkSize && + audioMetadata_.freeSpace() > sizeof(detail::DecodedChunkDescriptor)) { + + // The chunk descriptor for the chunk to be decoded + detail::DecodedChunkDescriptor descriptor{}; + descriptor.playbackGeneration_ = playbackGeneration_.load(std::memory_order_acquire); + descriptor.sequenceNumber_ = decoderState->sequenceNumber_; + // Decoding started - if (const auto flags = decoderState->loadFlags(); - bits::is_clear(flags, DecoderState::Flags::decodingStarted)) { - const auto suspended = bits::is_set(flags, DecoderState::Flags::decodingSuspended); + if (const auto decoderFlags = decoderState->loadFlags(); + bits::is_clear(decoderFlags, DecoderState::Flags::decodingStarted)) { + const auto suspended = bits::is_set(decoderFlags, DecoderState::Flags::decodingSuspended); if (!suspended) { os_log_debug(log_, "Decoding starting for %{public}@", decoderState->decoder_); @@ -1492,6 +1517,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } } + descriptor.framePosition_ = decoderState->framesDecoded_.load(std::memory_order_acquire); + // Decode audio into the buffer, converting to the rendering format in the process if (NSError *error = nil; !decoderState->decodeAudio(buffer, &error)) { decoderState->error_ = error; @@ -1499,8 +1526,17 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re goto next_outer_iteration; } - // Write the decoded audio to the ring buffer for rendering - const auto framesWritten = audioRingBuffer_.write(buffer.audioBufferList, buffer.frameLength); + descriptor.frameLength_ = buffer.frameLength; + + // Write the decoded chunk descriptor to the metadata buffer + if (!audioMetadata_.write(descriptor)) { + os_log_fault( + log_, + "Error writing audio chunk descriptor to ring buffer: spsc::RingBuffer::write failed"); + } + + // Write the decoded audio to the audio buffer for rendering + const auto framesWritten = audioBuffer_.write(buffer.audioBufferList, buffer.frameLength); if (framesWritten != buffer.frameLength) { os_log_fault( log_, @@ -1509,9 +1545,9 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } // Decoding complete - if (const auto flags = decoderState->loadFlags(); - bits::is_set(flags, DecoderState::Flags::decodingComplete)) { - const auto resumed = bits::is_set(flags, DecoderState::Flags::decodingResumed); + if (const auto decoderFlags = decoderState->loadFlags(); + bits::is_set(decoderFlags, DecoderState::Flags::decodingComplete)) { + const auto resumed = bits::is_set(decoderFlags, DecoderState::Flags::decodingResumed); // Submit the decoding complete event for the first completion only if (!resumed) { @@ -1552,14 +1588,14 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } else { // Determine timeout based on ring buffer free space // Attempt to keep the ring buffer 75% full - const auto targetMaxFreeSpace = audioRingBuffer_.capacity() / 4; - const auto freeSpace = audioRingBuffer_.freeSpace(); + const auto targetMaxFreeSpace = audioBuffer_.capacity() / 4; + const auto freeSpace = audioBuffer_.freeSpace(); if (freeSpace > targetMaxFreeSpace) { // Minimal timeout if the ring buffer has more free space than desired deltaNanos = static_cast(2.5 * NSEC_PER_MSEC); } else { - const auto duration = (targetMaxFreeSpace - freeSpace) / audioRingBuffer_.format().mSampleRate; + const auto duration = (targetMaxFreeSpace - freeSpace) / audioBuffer_.format().mSampleRate; deltaNanos = static_cast(duration * NSEC_PER_SEC); } } @@ -1588,7 +1624,9 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Discard any stale frames in the ring buffer from a seek or decoder cancelation if (bits::is_set(flags, Flags::drainRequired)) { - audioRingBuffer_.drain(); + audioBuffer_.drain(); + audioMetadata_.drain(); + std::memset(&renderingChunk_, 0, sizeof renderingChunk_); clearFlags(Flags::drainRequired); zeroABL(outputData); isSilence = YES; @@ -1602,20 +1640,49 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re return noErr; } + /// Computes the event time for a given frame offset + const auto eventTimeForFrameOffset = [&](uint32_t frameOffset) noexcept -> uint64_t { + const auto deltaSeconds = frameOffset / audioBuffer_.format().mSampleRate; + const auto scaledNanos = static_cast(deltaSeconds * timestamp.mRateScalar * nanosecondsPerSecond); + return timestamp.mHostTime + host_time::fromNanoseconds(scaledNanos); + }; + // Read audio from the ring buffer - const auto framesRead = audioRingBuffer_.read(outputData, frameCount); + const auto framesRead = static_cast(audioBuffer_.read(outputData, frameCount)); + + // Read and process chunk descriptors for the rendered audio if (framesRead > 0) { - if (!events_.enqueue(EventCommand::framesRendered, timestamp.mHostTime, timestamp.mRateScalar, - static_cast(framesRead))) { - setFlags(Flags::renderEventDropped); - } + auto framesRemaining = framesRead; + do { + // Read the next chunk descriptor if needed + if (renderingChunk_.framesRemaining() == 0) { + if (!audioMetadata_.read(renderingChunk_.descriptor_)) { + setFlags(Flags::renderEventDropped); + break; + } + renderingChunk_.framesConsumed_ = 0; + } + + const auto chunkFrames = std::min(renderingChunk_.framesRemaining(), framesRemaining); + if (chunkFrames > 0) { + const auto eventTime = eventTimeForFrameOffset(framesRead - framesRemaining); + if (!events_.enqueue(EventCommand::framesRendered, eventTime, + renderingChunk_.descriptor_.sequenceNumber_, chunkFrames, + renderingChunk_.descriptor_.playbackGeneration_)) { + setFlags(Flags::renderEventDropped); + break; + } + + renderingChunk_.framesConsumed_ += chunkFrames; + framesRemaining -= chunkFrames; + } + } while (framesRemaining > 0); } else { isSilence = YES; } if (framesRead != frameCount) { - if (!events_.enqueue(EventCommand::renderBufferUnderrun, timestamp.mHostTime, static_cast(framesRead), - static_cast(frameCount))) { + if (!events_.enqueue(EventCommand::renderBufferUnderrun, timestamp.mHostTime, framesRead, frameCount)) { setFlags(Flags::renderEventDropped); } } @@ -1710,9 +1777,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re { std::lock_guard lock{activeDecodersMutex_}; - if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); - iter != activeDecoders_.cend()) { - decoder = (*iter)->decoder_; + if (const auto *decoderState = decoderStateWithSequenceNumber(sequenceNumber); decoderState != nullptr) { + decoder = decoderState->decoder_; } else { os_log_error(log_, "Decoder state with sequence number %llu missing for decoding started event", sequenceNumber); @@ -1752,9 +1818,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re { std::lock_guard lock{activeDecodersMutex_}; - if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); - iter != activeDecoders_.cend()) { - decoder = (*iter)->decoder_; + if (const auto *decoderState = decoderStateWithSequenceNumber(sequenceNumber); decoderState != nullptr) { + decoder = decoderState->decoder_; } else { os_log_error(log_, "Decoder state with sequence number %llu missing for decoding complete event", sequenceNumber); @@ -1787,9 +1852,12 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re { std::lock_guard lock{activeDecodersMutex_}; - if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); - iter != activeDecoders_.cend()) { - decoder = (*iter)->decoder_; + if (auto *decoderState = decoderStateWithSequenceNumber(sequenceNumber); decoderState != nullptr) { + decoderState->framesRendered_.store(frame, std::memory_order_release); + if (bits::is_clear(decoderState->loadFlags(), DecoderState::Flags::renderingStarted)) { + return true; + } + decoder = decoderState->decoder_; } else { os_log_error(log_, "Decoder state with sequence number %llu missing for decoder seek event", sequenceNumber); @@ -1900,25 +1968,38 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re bool sfb::AudioPlayer::processFramesRenderedEvent() noexcept { EventCommand command; - // The host time and rate scalar from the render cycle's timestamp - uint64_t hostTime; - double rateScalar; + // The event time calculated from the render cycle's host time and rate scalar + uint64_t eventTime; + // The decoder sequence number for the decoder providing the frames + uint64_t sequenceNumber; // The number of valid frames rendered - uint32_t framesRendered; - if (!events_.dequeue(command, hostTime, rateScalar, framesRendered)) { - os_log_error(log_, "Missing timestamp or frames rendered for frames rendered event"); + uint32_t frameCount; + // The playback generation of the chunk containing the frames + uint64_t playbackGeneration; + if (!events_.dequeue(command, eventTime, sequenceNumber, frameCount, playbackGeneration)) { + os_log_error(log_, "Missing event time, decoder sequence number, frame count, or playback generation for " + "frames rendered event"); return false; } #if DEBUG assert(command == EventCommand::framesRendered); - assert(framesRendered > 0); + assert(frameCount > 0); #endif /* DEBUG */ - // Perform bookkeeping to apportion the rendered frames appropriately + // If a frames rendered event was posted it means valid frames were rendered + // during that render cycle. + // + // However, between the time the frames rendered event was queued and when it is processed + // a decoder may have been canceled or a seek may have occurred, making the event stale. + // + // This is indicated by an increment in the transport epoch/playback generation. // - // framesRendered contains the number of valid frames that were rendered - // but they could have come from multiple decoders + // Discard stale events from previous playback generations. + if (playbackGeneration != playbackGeneration_.load(std::memory_order_acquire)) { + os_log_debug(log_, "Discarding stale frames rendered event"); + return true; + } struct RenderingEventDetails { enum class Type { @@ -1936,50 +2017,14 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re { std::lock_guard lock{activeDecodersMutex_}; - AVAudioFramePosition framesRemainingToDistribute = framesRendered; - - auto iter = activeDecoders_.cbegin(); - while (iter != activeDecoders_.cend()) { + if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); + iter != activeDecoders_.cend()) { const auto flags = (*iter)->loadFlags(); - // Skip uninitialized decoders - if (bits::is_set(flags, DecoderState::Flags::needsInitialization)) { - ++iter; - continue; - } - - // If a frames rendered event was posted it means valid frames were rendered - // during that render cycle. - // - // However, between the time the frames rendered event was posted and when it is processed - // - A decoder may have been canceled - // - A seek can occur - // - // Bookkeeping is handled no differently for canceled decoders but rendering notifications are suppressed - // - // In the case of a seek the frames from that event are not valid and should be discarded. - - const auto decoderFramesDecoded = (*iter)->framesDecoded_.load(std::memory_order_acquire); - const auto decoderFramesRendered = (*iter)->framesRendered_.load(std::memory_order_acquire); - const auto decoderFramesRemaining = decoderFramesDecoded - decoderFramesRendered; - - if (decoderFramesRemaining == 0) { -#if DEBUG - os_log_debug(log_, "Not accounting for %lld frames in frames rendered event", - framesRemainingToDistribute); -#endif /* DEBUG */ - break; - } - // Rendering is starting - if (bits::has_none(flags, DecoderState::Flags::isCanceled | DecoderState::Flags::renderingStarted)) { + if (bits::is_clear(flags, DecoderState::Flags::renderingStarted)) { (*iter)->setFlags(DecoderState::Flags::renderingStarted); - const auto frameOffset = framesRendered - framesRemainingToDistribute; - const auto deltaSeconds = frameOffset / (*iter)->sampleRate(); - const auto eventTime = hostTime + host_time::fromNanoseconds(static_cast( - deltaSeconds * rateScalar * nanosecondsPerSecond)); - try { queuedEvents.push_back({RenderingEventDetails::Type::willStart, (*iter)->decoder_, eventTime}); } catch (const std::exception &e) { @@ -1988,20 +2033,15 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } } - const auto framesFromThisDecoder = std::min(decoderFramesRemaining, framesRemainingToDistribute); - - (*iter)->framesRendered_.fetch_add(framesFromThisDecoder, std::memory_order_acq_rel); - framesRemainingToDistribute -= framesFromThisDecoder; + const auto framesDecoded = (*iter)->framesDecoded_.load(std::memory_order_acquire); + const auto framesRendered = (*iter)->framesRendered_.fetch_add(frameCount, std::memory_order_acq_rel); + const auto framesRemaining = framesDecoded - framesRendered; +#if DEBUG + assert(framesRemaining >= frameCount); +#endif /* DEBUG */ // Rendering is complete - if (bits::is_set_and_is_clear(flags, DecoderState::Flags::decodingComplete, - DecoderState::Flags::isCanceled) && - framesFromThisDecoder == decoderFramesRemaining) { - const auto frameOffset = framesRendered - framesRemainingToDistribute; - const auto deltaSeconds = frameOffset / (*iter)->sampleRate(); - const auto eventTime = hostTime + host_time::fromNanoseconds(static_cast( - deltaSeconds * rateScalar * nanosecondsPerSecond)); - + if (bits::is_set(flags, DecoderState::Flags::decodingComplete) && frameCount == framesRemaining) { try { queuedEvents.push_back({RenderingEventDetails::Type::willComplete, (*iter)->decoder_, eventTime}); } catch (const std::exception &e) { @@ -2010,15 +2050,12 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } os_log_debug(log_, "Deleting decoder state for %{public}@", (*iter)->decoder_); - iter = activeDecoders_.erase(iter); - } else { - ++iter; - } - - // All frames processed - if (framesRemainingToDistribute == 0) { - break; + activeDecoders_.erase(iter); } + } else { + os_log_error(log_, "Decoder state with sequence number %llu missing for frames rendered event", + sequenceNumber); + return false; } } @@ -2244,15 +2281,19 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re #if DEBUG activeDecodersMutex_.assertIsOwner(); #endif /* DEBUG */ - const auto iter = std::ranges::find_if(activeDecoders_, [](const auto &decoderState) noexcept { const auto flags = decoderState->loadFlags(); return bits::has_none(flags, DecoderState::Flags::needsInitialization | DecoderState::Flags::isCanceled); }); - if (iter == activeDecoders_.cend()) { - return nullptr; - } - return iter->get(); + return iter != activeDecoders_.cend() ? iter->get() : nullptr; +} + +auto sfb::AudioPlayer::decoderStateWithSequenceNumber(uint64_t sequenceNumber) const noexcept -> DecoderState * { +#if DEBUG + activeDecodersMutex_.assertIsOwner(); +#endif /* DEBUG */ + const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); + return iter != activeDecoders_.cend() ? iter->get() : nullptr; } // MARK: - AVAudioEngine Notification Handling @@ -2463,11 +2504,11 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Allocate a temporary ring buffer for the new format before touching the engine or graph spsc::AudioRingBuffer ringBuffer; - if (!ringBuffer.allocate(*(format.streamDescription), ringBufferCapacity)) { + if (!ringBuffer.allocate(*(format.streamDescription), audioBufferCapacity)) { os_log_error(log_, - "Unable to create audio ring buffer: spsc::AudioRingBuffer::allocate failed with format " + "Unable to create audio buffer: spsc::AudioRingBuffer::allocate failed with format " "%{public}@ and capacity %zu", - SFBASBDFormatDescription(format.streamDescription), ringBufferCapacity); + SFBASBDFormatDescription(format.streamDescription), audioBufferCapacity); if (error != nullptr) { *error = [NSError errorWithDomain:SFBAudioPlayerErrorDomain code:SFBAudioPlayerErrorCodeInternalError @@ -2493,7 +2534,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Adopt the new ring buffer // The move is not thread-safe but the engine is stopped - audioRingBuffer_ = std::move(ringBuffer); + audioBuffer_ = std::move(ringBuffer); // Reconnect the source node to the next node in the processing chain // This is the mixer node in the default configuration, but additional nodes may From 29d453f18f2e3354d1d9cfdac1fae6089f362df1 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 07:48:34 -0600 Subject: [PATCH 02/20] Reformat --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 7bb0a969..49531d68 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -457,7 +457,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re const auto framePosition = decoder_.framePosition; if (framePosition == SFBUnknownFramePosition) { - os_log_error(log_, "Unknown frame position in %{public}@ after seeking to frame %lld", decoder_, requestedFrame); + os_log_error(log_, "Unknown frame position in %{public}@ after seeking to frame %lld", decoder_, + requestedFrame); return std::nullopt; } if (framePosition != requestedFrame) { @@ -1301,8 +1302,10 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re continue; } - nextDecoderState->framesDecoded_.store(framePosition.value(), std::memory_order_release); - if (events_.enqueue(EventCommand::seek, nextDecoderState->sequenceNumber_, framePosition.value())) { + nextDecoderState->framesDecoded_.store(framePosition.value(), + std::memory_order_release); + if (events_.enqueue(EventCommand::seek, nextDecoderState->sequenceNumber_, + framePosition.value())) { eventSemaphore_.signal(); } else { os_log_fault(log_, "Error writing decoder seek event"); From c78a873fdcc0b8a9aa613d96982bbe57d1b4d5b2 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 07:53:27 -0600 Subject: [PATCH 03/20] Rename variables --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 49531d68..0a4548a8 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1189,8 +1189,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re auto anyCanceled = false; for (const auto &decoderState : activeDecoders_) { - const auto flags = decoderState->loadFlags(); - if (bits::is_set_or_is_clear(flags, DecoderState::Flags::isCanceled, + const auto decoderFlags = decoderState->loadFlags(); + if (bits::is_set_or_is_clear(decoderFlags, DecoderState::Flags::isCanceled, DecoderState::Flags::cancelRequested)) { continue; } @@ -1201,7 +1201,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re os_log_error(log_, "Aborting decoding for %{public}@ due to error", decoderState->decoder_); } - if (bits::is_set(flags, DecoderState::Flags::decodingStarted)) { + if (bits::is_set(decoderFlags, DecoderState::Flags::decodingStarted)) { // Drain the ring buffer since the decoder could have contributed stale frames setFlags(Flags::drainRequired); @@ -1282,12 +1282,12 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re continue; } - const auto flags = nextDecoderState->loadFlags(); - if (bits::is_set(flags, DecoderState::Flags::isCanceled)) { + const auto nextDecoderFlags = nextDecoderState->loadFlags(); + if (bits::is_set(nextDecoderFlags, DecoderState::Flags::isCanceled)) { continue; } - if (bits::is_set(flags, DecoderState::Flags::decodingStarted)) { + if (bits::is_set(nextDecoderFlags, DecoderState::Flags::decodingStarted)) { os_log_debug(log_, "Suspending decoding for %{public}@", nextDecoderState->decoder_); // TODO: Investigate a per-state buffer to mitigate frame loss @@ -1334,15 +1334,12 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re std::lock_guard lock{activeDecodersMutex_}; const auto iter = std::ranges::find_if(activeDecoders_, [](const auto &decoderState) noexcept { - const auto flags = decoderState->loadFlags(); - return bits::has_none(flags, DecoderState::Flags::isCanceled | DecoderState::Flags::decodingComplete); + const auto decoderFlags = decoderState->loadFlags(); + return bits::has_none(decoderFlags, + DecoderState::Flags::isCanceled | DecoderState::Flags::decodingComplete); }); - if (iter != activeDecoders_.cend()) { - decoderState = iter->get(); - } else { - decoderState = nullptr; - } + decoderState = iter != activeDecoders_.cend() ? iter->get() : nullptr; } // Dequeue the next decoder if there are no decoders that haven't completed decoding @@ -2022,10 +2019,10 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); iter != activeDecoders_.cend()) { - const auto flags = (*iter)->loadFlags(); + const auto decoderFlags = (*iter)->loadFlags(); // Rendering is starting - if (bits::is_clear(flags, DecoderState::Flags::renderingStarted)) { + if (bits::is_clear(decoderFlags, DecoderState::Flags::renderingStarted)) { (*iter)->setFlags(DecoderState::Flags::renderingStarted); try { @@ -2044,7 +2041,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re #endif /* DEBUG */ // Rendering is complete - if (bits::is_set(flags, DecoderState::Flags::decodingComplete) && frameCount == framesRemaining) { + if (bits::is_set(decoderFlags, DecoderState::Flags::decodingComplete) && frameCount == framesRemaining) { try { queuedEvents.push_back({RenderingEventDetails::Type::willComplete, (*iter)->decoder_, eventTime}); } catch (const std::exception &e) { @@ -2285,8 +2282,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re activeDecodersMutex_.assertIsOwner(); #endif /* DEBUG */ const auto iter = std::ranges::find_if(activeDecoders_, [](const auto &decoderState) noexcept { - const auto flags = decoderState->loadFlags(); - return bits::has_none(flags, DecoderState::Flags::needsInitialization | DecoderState::Flags::isCanceled); + const auto decoderFlags = decoderState->loadFlags(); + return bits::has_none(decoderFlags, DecoderState::Flags::needsInitialization | DecoderState::Flags::isCanceled); }); return iter != activeDecoders_.cend() ? iter->get() : nullptr; } From 3fdc654aa8b783bfc84ccfe94f0649f2a8647bca Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 08:07:30 -0600 Subject: [PATCH 04/20] Use `>=` for free space check --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 0a4548a8..4b31b995 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1486,7 +1486,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (const auto flags = loadFlags(); bits::is_clear(flags, Flags::drainRequired)) { // Decode and write chunks and metadata to the ring buffers while (audioBuffer_.freeSpace() >= ringBufferChunkSize && - audioMetadata_.freeSpace() > sizeof(detail::DecodedChunkDescriptor)) { + audioMetadata_.freeSpace() >= sizeof(detail::DecodedChunkDescriptor)) { // The chunk descriptor for the chunk to be decoded detail::DecodedChunkDescriptor descriptor{}; From ef6d74990b395405948adafa783850f2f25ba3a1 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 08:29:20 -0600 Subject: [PATCH 05/20] Set mute flag when reconfiguring --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 4b31b995..4a0ab366 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1448,11 +1448,17 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re }(); if (okToReconfigure) { - // clearFlags(Flags::drainRequired); - formatMismatch = false; - os_log_debug(log_, "Non-gapless join for %{public}@", decoderState->decoder_); + formatMismatch = false; + fetchUpdate( + flags_, + [](auto val) noexcept { + return (val & ~bits::to_underlying(Flags::drainRequired)) | + bits::to_underlying(Flags::isMuted); + }, + std::memory_order_acq_rel); + auto renderFormat = decoderState->converter_.outputFormat; if (NSError *error = nil; !configureProcessingGraphAndRingBufferForFormat(renderFormat, &error)) { decoderState->error_ = error; From f26532c735a8f38abc1e7215837ef4c38169aea7 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 10:52:58 -0600 Subject: [PATCH 06/20] Effectively perform a drain in `configureProcessingGraphAndRingBufferForFormat` --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 4a0ab366..dcc8905e 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1450,15 +1450,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (okToReconfigure) { os_log_debug(log_, "Non-gapless join for %{public}@", decoderState->decoder_); - formatMismatch = false; - fetchUpdate( - flags_, - [](auto val) noexcept { - return (val & ~bits::to_underlying(Flags::drainRequired)) | - bits::to_underlying(Flags::isMuted); - }, - std::memory_order_acq_rel); - auto renderFormat = decoderState->converter_.outputFormat; if (NSError *error = nil; !configureProcessingGraphAndRingBufferForFormat(renderFormat, &error)) { decoderState->error_ = error; @@ -1466,6 +1457,9 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re continue; } + clearFlags(Flags::drainRequired); + formatMismatch = false; + // Allocate the buffer that is the intermediary between the decoder state and the ring buffer if (auto format = buffer.format; format.channelCount != renderFormat.channelCount || format.sampleRate != renderFormat.sampleRate) { @@ -1632,7 +1626,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (bits::is_set(flags, Flags::drainRequired)) { audioBuffer_.drain(); audioMetadata_.drain(); - std::memset(&renderingChunk_, 0, sizeof renderingChunk_); + renderingChunk_ = {}; clearFlags(Flags::drainRequired); zeroABL(outputData); isSilence = YES; @@ -2538,9 +2532,11 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re outputBus:0] firstObject]; [engine_ disconnectNodeOutput:sourceNode_]; - // Adopt the new ring buffer - // The move is not thread-safe but the engine is stopped + // Adopt the new ring buffer and reset the render state + // These operations are not thread-safe but the engine is stopped audioBuffer_ = std::move(ringBuffer); + audioMetadata_.drain(); + renderingChunk_ = {}; // Reconnect the source node to the next node in the processing chain // This is the mixer node in the default configuration, but additional nodes may From 5eb5238e0166fa9f00be77aedec5c8b83aa8195b Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 11:08:00 -0600 Subject: [PATCH 07/20] Add helper lambda --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index dcc8905e..4d7e48f4 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1177,6 +1177,12 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Whether there is a mismatch between the rendering format and the next decoder's processing format auto formatMismatch = false; + /// Sets the decoder state's error and cancellation request flag + const auto setErrorAndRequestCancel = [](DecoderState *_Nonnull decoderState, NSError *_Nonnull error) noexcept { + decoderState->error_ = error; + decoderState->setFlags(DecoderState::Flags::cancelRequested); + }; + while (!stoken.stop_requested()) { // The decoder state being processed DecoderState *decoderState = nullptr; @@ -1239,8 +1245,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re NSError *seekError = nil; const auto framePosition = decoderState->performSeek(&seekError); if (!framePosition.has_value()) { - decoderState->error_ = seekError; - decoderState->setFlags(DecoderState::Flags::cancelRequested); + setErrorAndRequestCancel(decoderState, seekError); continue; } @@ -1297,8 +1302,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re NSError *seekError = nil; const auto framePosition = nextDecoderState->performSeek(&seekError); if (!framePosition.has_value()) { - nextDecoderState->error_ = seekError; - nextDecoderState->setFlags(DecoderState::Flags::cancelRequested); + setErrorAndRequestCancel(decoderState, seekError); continue; } @@ -1380,8 +1384,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (!decoderState->decoder_.isOpen) { if (NSError *error = nil; ![decoderState->decoder_ openReturningError:&error]) { os_log_error(log_, "Error opening %{public}@: %{public}@", decoderState->decoder_, error); - decoderState->error_ = error; - decoderState->setFlags(DecoderState::Flags::cancelRequested); + setErrorAndRequestCancel(decoderState, error); continue; } @@ -1397,10 +1400,9 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re "Error allocating decoder state data: DecoderStateData::allocate failed with frame " "capacity %u", ringBufferChunkSize); - decoderState->error_ = [NSError errorWithDomain:SFBAudioPlayerErrorDomain - code:SFBAudioPlayerErrorCodeInternalError - userInfo:nil]; - decoderState->setFlags(DecoderState::Flags::cancelRequested); + setErrorAndRequestCancel(decoderState, [NSError errorWithDomain:SFBAudioPlayerErrorDomain + code:SFBAudioPlayerErrorCodeInternalError + userInfo:nil]); continue; } @@ -1424,10 +1426,10 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re os_log_error(log_, "Error creating AVAudioPCMBuffer with format %{public}@ and frame capacity %u", stringDescribingAVAudioFormat(renderFormat), ringBufferChunkSize); - decoderState->error_ = [NSError errorWithDomain:SFBAudioPlayerErrorDomain - code:SFBAudioPlayerErrorCodeInternalError - userInfo:nil]; - decoderState->setFlags(DecoderState::Flags::cancelRequested); + setErrorAndRequestCancel(decoderState, + [NSError errorWithDomain:SFBAudioPlayerErrorDomain + code:SFBAudioPlayerErrorCodeInternalError + userInfo:nil]); continue; } } @@ -1452,8 +1454,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re auto renderFormat = decoderState->converter_.outputFormat; if (NSError *error = nil; !configureProcessingGraphAndRingBufferForFormat(renderFormat, &error)) { - decoderState->error_ = error; - decoderState->setFlags(DecoderState::Flags::cancelRequested); + setErrorAndRequestCancel(decoderState, error); continue; } @@ -1469,10 +1470,10 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re os_log_error(log_, "Error creating AVAudioPCMBuffer with format %{public}@ and frame capacity %u", stringDescribingAVAudioFormat(renderFormat), ringBufferChunkSize); - decoderState->error_ = [NSError errorWithDomain:SFBAudioPlayerErrorDomain - code:SFBAudioPlayerErrorCodeInternalError - userInfo:nil]; - decoderState->setFlags(DecoderState::Flags::cancelRequested); + setErrorAndRequestCancel(decoderState, + [NSError errorWithDomain:SFBAudioPlayerErrorDomain + code:SFBAudioPlayerErrorCodeInternalError + userInfo:nil]); continue; } } @@ -1521,8 +1522,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Decode audio into the buffer, converting to the rendering format in the process if (NSError *error = nil; !decoderState->decodeAudio(buffer, &error)) { - decoderState->error_ = error; - decoderState->setFlags(DecoderState::Flags::cancelRequested); + setErrorAndRequestCancel(decoderState, error); goto next_outer_iteration; } From b9f5e21082f71f7f91569dbd341c3739ea9773ff Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 18:48:50 -0600 Subject: [PATCH 08/20] Replace `CXXRingBuffer` dependency with `CXXQueue` --- Package.resolved | 8 ++-- Package.swift | 4 +- Sources/CSFBAudioEngine/Player/AudioPlayer.h | 6 +-- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 26 +++-------- Sources/CSFBAudioEngine/Player/scope_exit.hpp | 43 +++++++++++++++++++ 5 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 Sources/CSFBAudioEngine/Player/scope_exit.hpp diff --git a/Package.resolved b/Package.resolved index 9bea48cd..cc21ecd3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -64,12 +64,12 @@ } }, { - "identity" : "cxxringbuffer", + "identity" : "cxxqueue", "kind" : "remoteSourceControl", - "location" : "https://github.com/sbooth/CXXRingBuffer", + "location" : "https://github.com/sbooth/CXXQueue", "state" : { - "revision" : "8d859aa73d115273afcebe754b22bf50d3ea0621", - "version" : "0.6.1" + "branch" : "main", + "revision" : "cd1a618382176d8724b82025b7caab611ae39607" } }, { diff --git a/Package.swift b/Package.swift index 9f883179..50e231d4 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,7 @@ let package = Package( .package(url: "https://github.com/sbooth/CXXAudioRingBuffer", .upToNextMinor(from: "0.1.1")), .package(url: "https://github.com/sbooth/CXXDispatchSemaphore", .upToNextMinor(from: "0.4.1")), .package(url: "https://github.com/sbooth/CXXMessageQueue", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/sbooth/CXXRingBuffer", .upToNextMinor(from: "0.6.1")), + .package(url: "https://github.com/sbooth/CXXQueue", branch: "main"), .package(url: "https://github.com/sbooth/CXXUnfairLock", .upToNextMinor(from: "0.3.1")), // Standalone dependencies from source @@ -67,7 +67,7 @@ let package = Package( .product(name: "CXXAudioRingBuffer", package: "CXXAudioRingBuffer"), .product(name: "CXXDispatchSemaphore", package: "CXXDispatchSemaphore"), .product(name: "CXXMessageQueue", package: "CXXMessageQueue"), - .product(name: "CXXRingBuffer", package: "CXXRingBuffer"), + .product(name: "CXXQueue", package: "CXXQueue"), .product(name: "CXXUnfairLock", package: "CXXUnfairLock"), // Standalone dependencies .product(name: "dumb", package: "CDUMB"), diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index 8915b488..bc0f8e0d 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -15,7 +15,7 @@ #import #import #import -#import +#import #import @@ -85,8 +85,8 @@ class AudioPlayer final { /// Ring buffer transferring audio between the decoding thread and the render block spsc::AudioRingBuffer audioBuffer_; - /// Ring buffer transferring audio metadata between the decoding thread and the render block - spsc::RingBuffer audioMetadata_; + /// Queue transferring audio metadata between the decoding thread and the render block + spsc::Queue audioMetadata_; /// The current transport epoch std::atomic playbackGeneration_{1}; static_assert(std::atomic::is_always_lock_free, "Lock-free std::atomic required"); diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 4d7e48f4..5d5054a6 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -32,8 +32,6 @@ /// The default audio ring buffer capacity in frames constexpr std::size_t audioBufferCapacity = 16384; -/// The default audio metadata buffer capacity in bytes -constexpr std::size_t metadataBufferCapacity = 2048; /// The minimum number of frames to write to the audio ring buffer constexpr AVAudioFrameCount ringBufferChunkSize = 2048; @@ -492,13 +490,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re throw std::runtime_error("spsc::AudioRingBuffer::allocate failed"); } - // Allocate the metadata buffer carrying decoded chunk descriptors from the decoder thread to the render block - if (!audioMetadata_.allocate(metadataBufferCapacity)) { - os_log_error(log_, "Unable to create metadata buffer: spsc::RingBuffer::allocate failed with capacity %zu", - metadataBufferCapacity); - throw std::runtime_error("spsc::RingBuffer::allocate failed"); - } - // ======================================== // Event Processing Setup @@ -1486,8 +1477,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (decoderState != nullptr) { if (const auto flags = loadFlags(); bits::is_clear(flags, Flags::drainRequired)) { // Decode and write chunks and metadata to the ring buffers - while (audioBuffer_.freeSpace() >= ringBufferChunkSize && - audioMetadata_.freeSpace() >= sizeof(detail::DecodedChunkDescriptor)) { + while (audioBuffer_.freeSpace() >= ringBufferChunkSize && !audioMetadata_.isFull()) { // The chunk descriptor for the chunk to be decoded detail::DecodedChunkDescriptor descriptor{}; @@ -1529,19 +1519,15 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re descriptor.frameLength_ = buffer.frameLength; // Write the decoded chunk descriptor to the metadata buffer - if (!audioMetadata_.write(descriptor)) { - os_log_fault( - log_, - "Error writing audio chunk descriptor to ring buffer: spsc::RingBuffer::write failed"); + if (!audioMetadata_.push(descriptor)) { + os_log_fault(log_, "Error writing chunk descriptor: spsc::Queue::push failed"); } // Write the decoded audio to the audio buffer for rendering const auto framesWritten = audioBuffer_.write(buffer.audioBufferList, buffer.frameLength); if (framesWritten != buffer.frameLength) { - os_log_fault( - log_, - "Error writing audio to ring buffer: spsc::AudioRingBuffer::write failed for %u frames", - buffer.frameLength); + os_log_fault(log_, "Error writing audio: spsc::AudioRingBuffer::write failed for %u frames", + buffer.frameLength); } // Decoding complete @@ -1656,7 +1642,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re do { // Read the next chunk descriptor if needed if (renderingChunk_.framesRemaining() == 0) { - if (!audioMetadata_.read(renderingChunk_.descriptor_)) { + if (!audioMetadata_.pop(renderingChunk_.descriptor_)) { setFlags(Flags::renderEventDropped); break; } diff --git a/Sources/CSFBAudioEngine/Player/scope_exit.hpp b/Sources/CSFBAudioEngine/Player/scope_exit.hpp new file mode 100644 index 00000000..38933e06 --- /dev/null +++ b/Sources/CSFBAudioEngine/Player/scope_exit.hpp @@ -0,0 +1,43 @@ +// +// SPDX-FileCopyrightText: 2026 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#pragma once + +#include +#include +#include + +namespace util { + +template +concept nothrow_invocable = std::invocable && std::is_nothrow_invocable_v; + +/// A simple scope guard +template class scope_exit final { + public: + explicit scope_exit(F f) noexcept : f_(std::move(f)), active_(true) {} + + ~scope_exit() { + if (active_) { + f_(); + } + } + + scope_exit(const scope_exit &) = delete; + scope_exit &operator=(const scope_exit &) = delete; + + scope_exit(scope_exit &&other) noexcept : f_(std::move(other.f_)), active_(other.active_) { other.active_ = false; } + scope_exit &operator=(scope_exit &&) = delete; + + private: + F f_; + bool active_; +}; + +template scope_exit(F) -> scope_exit; + +} /* namespace util */ From b11eb7b74b3eaf77b21b7977dd6dcf40d1339665 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 18:53:08 -0600 Subject: [PATCH 09/20] Remove accidental commit --- Sources/CSFBAudioEngine/Player/scope_exit.hpp | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 Sources/CSFBAudioEngine/Player/scope_exit.hpp diff --git a/Sources/CSFBAudioEngine/Player/scope_exit.hpp b/Sources/CSFBAudioEngine/Player/scope_exit.hpp deleted file mode 100644 index 38933e06..00000000 --- a/Sources/CSFBAudioEngine/Player/scope_exit.hpp +++ /dev/null @@ -1,43 +0,0 @@ -// -// SPDX-FileCopyrightText: 2026 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#pragma once - -#include -#include -#include - -namespace util { - -template -concept nothrow_invocable = std::invocable && std::is_nothrow_invocable_v; - -/// A simple scope guard -template class scope_exit final { - public: - explicit scope_exit(F f) noexcept : f_(std::move(f)), active_(true) {} - - ~scope_exit() { - if (active_) { - f_(); - } - } - - scope_exit(const scope_exit &) = delete; - scope_exit &operator=(const scope_exit &) = delete; - - scope_exit(scope_exit &&other) noexcept : f_(std::move(other.f_)), active_(other.active_) { other.active_ = false; } - scope_exit &operator=(scope_exit &&) = delete; - - private: - F f_; - bool active_; -}; - -template scope_exit(F) -> scope_exit; - -} /* namespace util */ From f8a8e44971c6612f620ae81fba4d83313b834870 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 19 Jul 2026 21:59:35 -0600 Subject: [PATCH 10/20] Adjust memory order --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 5d5054a6..92f02061 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -390,7 +390,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re return true; } - this->framesDecoded_.fetch_add(framesDecoded, std::memory_order_acq_rel); + this->framesDecoded_.fetch_add(framesDecoded, std::memory_order_release); // Only PCM to PCM conversions are performed if (![converter_ convertToBuffer:buffer fromBuffer:decodeBuffer_ error:error]) { @@ -1203,7 +1203,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re setFlags(Flags::drainRequired); // Increment the playback epoch to expire any inflight events - playbackGeneration_.fetch_add(1, std::memory_order_acq_rel); + playbackGeneration_.fetch_add(1, std::memory_order_release); } decoderState->setFlags(DecoderState::Flags::isCanceled); @@ -1244,7 +1244,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re setFlags(Flags::isMuted | Flags::drainRequired); // Increment the playback epoch to expire any inflight events - playbackGeneration_.fetch_add(1, std::memory_order_acq_rel); + playbackGeneration_.fetch_add(1, std::memory_order_release); decoderState->framesDecoded_.store(framePosition.value(), std::memory_order_release); if (events_.enqueue(EventCommand::seek, decoderState->sequenceNumber_, framePosition.value())) { @@ -2020,7 +2020,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } const auto framesDecoded = (*iter)->framesDecoded_.load(std::memory_order_acquire); - const auto framesRendered = (*iter)->framesRendered_.fetch_add(frameCount, std::memory_order_acq_rel); + const auto framesRendered = (*iter)->framesRendered_.fetch_add(frameCount, std::memory_order_release); const auto framesRemaining = framesDecoded - framesRendered; #if DEBUG assert(framesRemaining >= frameCount); From 7a2d4d9c890b6fd1cb71f6ec77b95613b87f8da3 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Mon, 20 Jul 2026 06:43:52 -0600 Subject: [PATCH 11/20] Fix a race condition in frames rendered processing --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 92f02061..7ce6d048 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1243,10 +1243,17 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Mute until the seek is complete and the ring buffer is drained and refilled setFlags(Flags::isMuted | Flags::drainRequired); - // Increment the playback epoch to expire any inflight events - playbackGeneration_.fetch_add(1, std::memory_order_release); + { + // Ensure the playback epoch increment and frame counter updates occur together + std::lock_guard lock{activeDecodersMutex_}; + + // Increment the playback epoch to expire any inflight events + playbackGeneration_.fetch_add(1, std::memory_order_release); + + decoderState->framesDecoded_.store(framePosition.value(), std::memory_order_release); + decoderState->framesRendered_.store(framePosition.value(), std::memory_order_release); + } - decoderState->framesDecoded_.store(framePosition.value(), std::memory_order_release); if (events_.enqueue(EventCommand::seek, decoderState->sequenceNumber_, framePosition.value())) { eventSemaphore_.signal(); } else { @@ -1299,6 +1306,9 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re nextDecoderState->framesDecoded_.store(framePosition.value(), std::memory_order_release); + nextDecoderState->framesRendered_.store(framePosition.value(), + std::memory_order_release); + if (events_.enqueue(EventCommand::seek, nextDecoderState->sequenceNumber_, framePosition.value())) { eventSemaphore_.signal(); @@ -1839,10 +1849,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re std::lock_guard lock{activeDecodersMutex_}; if (auto *decoderState = decoderStateWithSequenceNumber(sequenceNumber); decoderState != nullptr) { - decoderState->framesRendered_.store(frame, std::memory_order_release); - if (bits::is_clear(decoderState->loadFlags(), DecoderState::Flags::renderingStarted)) { - return true; - } decoder = decoderState->decoder_; } else { os_log_error(log_, "Decoder state with sequence number %llu missing for decoder seek event", @@ -1981,11 +1987,12 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // // This is indicated by an increment in the transport epoch/playback generation. // - // Discard stale events from previous playback generations. - if (playbackGeneration != playbackGeneration_.load(std::memory_order_acquire)) { - os_log_debug(log_, "Discarding stale frames rendered event"); - return true; - } + // NB: The generation check must happen under activeDecodersMutex_, together with the frame + // counter reads below. A seek on the decoding thread bumps playbackGeneration_ and resets a + // decoder state's framesDecoded_/framesRendered_ under the same mutex (see the seek handling + // in the decoding loop). Checking the generation before taking the lock leaves a window in + // which the seek's reset can land between the check and the reads below, making a stale + // event's frame count get applied to post-seek counters. struct RenderingEventDetails { enum class Type { @@ -2003,6 +2010,12 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re { std::lock_guard lock{activeDecodersMutex_}; + // Discard stale events from previous playback generations + if (playbackGeneration != playbackGeneration_.load(std::memory_order_acquire)) { + os_log_debug(log_, "Discarding stale frames rendered event"); + return true; + } + if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); iter != activeDecoders_.cend()) { const auto decoderFlags = (*iter)->loadFlags(); @@ -2020,7 +2033,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } const auto framesDecoded = (*iter)->framesDecoded_.load(std::memory_order_acquire); - const auto framesRendered = (*iter)->framesRendered_.fetch_add(frameCount, std::memory_order_release); + const auto framesRendered = (*iter)->framesRendered_.fetch_add(frameCount, std::memory_order_acq_rel); const auto framesRemaining = framesDecoded - framesRendered; #if DEBUG assert(framesRemaining >= frameCount); From 0cc8e5e74c64add8edfd72ce0a95a2435a80764d Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Mon, 20 Jul 2026 18:28:10 -0600 Subject: [PATCH 12/20] Cancel the correct decoder --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 7ce6d048..16d92f94 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1300,7 +1300,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re NSError *seekError = nil; const auto framePosition = nextDecoderState->performSeek(&seekError); if (!framePosition.has_value()) { - setErrorAndRequestCancel(decoderState, seekError); + setErrorAndRequestCancel(nextDecoderState.get(), seekError); continue; } From 277b94d9166e182603cf0da43384335eb3c0bd10 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Mon, 20 Jul 2026 21:26:54 -0600 Subject: [PATCH 13/20] Remove unused function --- Sources/CSFBAudioEngine/Player/AudioPlayer.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index bc0f8e0d..c2f6606c 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -58,8 +58,6 @@ struct RenderingChunkDescriptor final { /// Returns the number of frames remaining in this chunk [[nodiscard]] uint32_t framesRemaining() const noexcept { return descriptor_.frameLength_ - framesConsumed_; } - /// Returns the frame position of the next frame in this chunk - [[nodiscard]] int64_t framePosition() const noexcept { return descriptor_.framePosition_ + framesConsumed_; } }; } /* namespace detail */ From f4d6176f438c235c876b3670bc127a96e0ba2ea5 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Wed, 22 Jul 2026 16:03:59 -0500 Subject: [PATCH 14/20] `CXXQueue` API updates --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 16d92f94..f5193532 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1621,7 +1621,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Discard any stale frames in the ring buffer from a seek or decoder cancelation if (bits::is_set(flags, Flags::drainRequired)) { audioBuffer_.drain(); - audioMetadata_.drain(); + audioMetadata_.discardAll(); renderingChunk_ = {}; clearFlags(Flags::drainRequired); zeroABL(outputData); @@ -2534,7 +2534,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Adopt the new ring buffer and reset the render state // These operations are not thread-safe but the engine is stopped audioBuffer_ = std::move(ringBuffer); - audioMetadata_.drain(); + audioMetadata_.discardAll(); renderingChunk_ = {}; // Reconnect the source node to the next node in the processing chain From 6bda02373f6dfe30092b4a48e8e31078b2b25170 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Wed, 22 Jul 2026 16:20:23 -0500 Subject: [PATCH 15/20] Update Package.resolved --- Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index cc21ecd3..054a5ced 100644 --- a/Package.resolved +++ b/Package.resolved @@ -69,7 +69,7 @@ "location" : "https://github.com/sbooth/CXXQueue", "state" : { "branch" : "main", - "revision" : "cd1a618382176d8724b82025b7caab611ae39607" + "revision" : "490f1822958098eb82bfbcfaadc50f5a9faafa6c" } }, { From 2f3f36385a2ffebc45a40658521a6640e764c727 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Wed, 22 Jul 2026 16:53:44 -0500 Subject: [PATCH 16/20] Update `CXXQueue` to version 0.1.0 --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 054a5ced..dc979f8f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sbooth/CXXQueue", "state" : { - "branch" : "main", - "revision" : "490f1822958098eb82bfbcfaadc50f5a9faafa6c" + "revision" : "7e4e21a78c4bde49d6b4f4ec4220dfbf81e8e1c5", + "version" : "0.1.0" } }, { diff --git a/Package.swift b/Package.swift index 50e231d4..2250b01c 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,7 @@ let package = Package( .package(url: "https://github.com/sbooth/CXXAudioRingBuffer", .upToNextMinor(from: "0.1.1")), .package(url: "https://github.com/sbooth/CXXDispatchSemaphore", .upToNextMinor(from: "0.4.1")), .package(url: "https://github.com/sbooth/CXXMessageQueue", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/sbooth/CXXQueue", branch: "main"), + .package(url: "https://github.com/sbooth/CXXQueue", .upToNextMinor(from: "0.1.0")), .package(url: "https://github.com/sbooth/CXXUnfairLock", .upToNextMinor(from: "0.3.1")), // Standalone dependencies from source From c497a3ba46893a3b8d96bc017adaf6fa06714f15 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Fri, 24 Jul 2026 18:58:15 -0500 Subject: [PATCH 17/20] Update CXXAudioRingBuffer to version 0.2.0 --- Package.resolved | 4 +- Package.swift | 2 +- Sources/CSFBAudioEngine/Player/AudioPlayer.h | 22 ++-- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 109 +++++++++--------- 4 files changed, 71 insertions(+), 66 deletions(-) diff --git a/Package.resolved b/Package.resolved index dc979f8f..45d79473 100644 --- a/Package.resolved +++ b/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sbooth/CXXAudioRingBuffer", "state" : { - "revision" : "95052b6bd4ccf192e2f0983d401224e7a4921e39", - "version" : "0.1.1" + "revision" : "266fb309d074f7843c9f125435b5921075c957b5", + "version" : "0.2.0" } }, { diff --git a/Package.swift b/Package.swift index 2250b01c..67777ddb 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/sbooth/AVFAudioExtensions", .upToNextMinor(from: "0.5.1")), - .package(url: "https://github.com/sbooth/CXXAudioRingBuffer", .upToNextMinor(from: "0.1.1")), + .package(url: "https://github.com/sbooth/CXXAudioRingBuffer", .upToNextMinor(from: "0.2.0")), .package(url: "https://github.com/sbooth/CXXDispatchSemaphore", .upToNextMinor(from: "0.4.1")), .package(url: "https://github.com/sbooth/CXXMessageQueue", .upToNextMinor(from: "0.2.0")), .package(url: "https://github.com/sbooth/CXXQueue", .upToNextMinor(from: "0.1.0")), diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index c2f6606c..83390bb1 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -233,13 +233,13 @@ class AudioPlayer final { /// Possible bits in `flags_` enum class Flags : unsigned int { /// Cached value of `engine_.isRunning` - engineIsRunning = 1u << 0, + engineRunning = 1u << 0, /// The render block should output audio - isPlaying = 1u << 1, + playing = 1u << 1, /// The render block should output silence - isMuted = 1u << 2, - /// The ring buffer needs to be drained during the next render cycle - drainRequired = 1u << 3, + muted = 1u << 2, + /// The ring buffer contains stale audio and needs to be emptied during the next render cycle + audioStale = 1u << 3, /// The event message queue had insufficient space to record a render event renderEventDropped = 1u << 4, }; @@ -281,7 +281,7 @@ class AudioPlayer final { /// Render block implementation OSStatus render(BOOL &isSilence, const AudioTimeStamp ×tamp, AVAudioFrameCount frameCount, - AudioBufferList *_Nonnull outputData) noexcept; + AudioBufferList &outputData) noexcept; /// The current rendering chunk descriptor detail::RenderingChunkDescriptor renderingChunk_{}; @@ -388,26 +388,26 @@ inline bool AudioPlayer::decoderQueueIsEmpty() const noexcept { inline SFBAudioPlayerPlaybackState AudioPlayer::playbackState() const noexcept { const auto flags = loadFlags(); - const auto state = flags & (Flags::engineIsRunning | Flags::isPlaying); + const auto state = flags & (Flags::engineRunning | Flags::playing); #if DEBUG - assert(bits::is_set_or_is_clear(state, Flags::engineIsRunning, Flags::isPlaying)); + assert(bits::is_set_or_is_clear(state, Flags::engineRunning, Flags::playing)); #endif /* DEBUG */ return static_cast(state); } inline bool AudioPlayer::isPlaying() const noexcept { const auto flags = loadFlags(); - return bits::has_all(flags, Flags::engineIsRunning | Flags::isPlaying); + return bits::has_all(flags, Flags::engineRunning | Flags::playing); } inline bool AudioPlayer::isPaused() const noexcept { const auto flags = loadFlags(); - return bits::is_set_and_is_clear(flags, Flags::engineIsRunning, Flags::isPlaying); + return bits::is_set_and_is_clear(flags, Flags::engineRunning, Flags::playing); } inline bool AudioPlayer::isStopped() const noexcept { const auto flags = loadFlags(); - return bits::is_clear(flags, Flags::engineIsRunning); + return bits::is_clear(flags, Flags::engineRunning); } inline bool AudioPlayer::isReady() const noexcept { diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index f5193532..c5bbcb31 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -208,7 +208,7 @@ constexpr T absoluteDifference(T a, T b) noexcept { /// Decoder cancelation requested cancelRequested = 1u << 6, /// Decoder canceled - isCanceled = 1u << 7, + canceled = 1u << 7, }; /// Flags @@ -527,7 +527,10 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re sourceNode_ = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus(BOOL *isSilence, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, AudioBufferList *outputData) { - return render(*isSilence, *timestamp, frameCount, outputData); +#if DEBUG + assert(outputData != nullptr); +#endif + return render(*isSilence, *timestamp, frameCount, *outputData); }]; if (sourceNode_ == nil) { throw std::runtime_error("Unable to create AVAudioSourceNode instance"); @@ -563,7 +566,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re { std::lock_guard lock{engineMutex_}; [engine_ stop]; - clearFlags(Flags::engineIsRunning | Flags::isPlaying); + clearFlags(Flags::engineRunning | Flags::playing); } clearDecoderQueue(); @@ -628,7 +631,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (forImmediatePlayback) { cancelActiveDecoders(); // Mute until the decoder becomes active - setFlags(Flags::isMuted); + setFlags(Flags::muted); } decodingSemaphore_.signal(); @@ -656,7 +659,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (didStartEngine = !engine_.isRunning; didStartEngine) { if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); - clearFlags(Flags::engineIsRunning | Flags::isPlaying); + clearFlags(Flags::engineRunning | Flags::playing); if (error != nullptr) { *error = startError; } @@ -664,8 +667,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } } - const auto prevFlags = setFlags(Flags::engineIsRunning | Flags::isPlaying); - wasPlaying = bits::is_set(prevFlags, Flags::isPlaying); + const auto prevFlags = setFlags(Flags::engineRunning | Flags::playing); + wasPlaying = bits::is_set(prevFlags, Flags::playing); #if DEBUG assert(!(didStartEngine && wasPlaying)); #endif /* DEBUG */ @@ -688,8 +691,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (!engine_.isRunning) { return false; } - const auto prevFlags = clearFlags(Flags::isPlaying); - wasPlaying = bits::is_set(prevFlags, Flags::isPlaying); + const auto prevFlags = clearFlags(Flags::playing); + wasPlaying = bits::is_set(prevFlags, Flags::playing); } if (wasPlaying) { @@ -709,8 +712,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (!engine_.isRunning) { return false; } - const auto prevFlags = setFlags(Flags::isPlaying); - wasPaused = bits::is_clear(prevFlags, Flags::isPlaying); + const auto prevFlags = setFlags(Flags::playing); + wasPaused = bits::is_clear(prevFlags, Flags::playing); } if (wasPaused) { @@ -746,23 +749,23 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re if (!engine_.isRunning) { if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); - clearFlags(Flags::engineIsRunning | Flags::isPlaying); + clearFlags(Flags::engineRunning | Flags::playing); if (error != nullptr) { *error = startError; } return false; } - [[maybe_unused]] const auto prevFlags = setFlags(Flags::engineIsRunning | Flags::isPlaying); + [[maybe_unused]] const auto prevFlags = setFlags(Flags::engineRunning | Flags::playing); #if DEBUG - assert(bits::is_clear(prevFlags, Flags::isPlaying)); + assert(bits::is_clear(prevFlags, Flags::playing)); #endif /* DEBUG */ playbackState = SFBAudioPlayerPlaybackStatePlaying; } else { // Toggle playing/paused - const auto prevFlags = toggleFlags(Flags::isPlaying); - if (bits::is_set(prevFlags, Flags::isPlaying)) { + const auto prevFlags = toggleFlags(Flags::playing); + if (bits::is_set(prevFlags, Flags::playing)) { playbackState = SFBAudioPlayerPlaybackStatePaused; } else { playbackState = SFBAudioPlayerPlaybackStatePlaying; @@ -792,7 +795,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re bool sfb::AudioPlayer::engineIsRunning() const noexcept { const auto isRunning = engine_.isRunning; #if DEBUG - assert(bits::is_set(loadFlags(), Flags::engineIsRunning) == isRunning && + assert(bits::is_set(loadFlags(), Flags::engineRunning) == isRunning && "Cached value for engine_.isRunning invalid"); #endif /* DEBUG */ return isRunning; @@ -1187,7 +1190,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re for (const auto &decoderState : activeDecoders_) { const auto decoderFlags = decoderState->loadFlags(); - if (bits::is_set_or_is_clear(decoderFlags, DecoderState::Flags::isCanceled, + if (bits::is_set_or_is_clear(decoderFlags, DecoderState::Flags::canceled, DecoderState::Flags::cancelRequested)) { continue; } @@ -1199,14 +1202,14 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } if (bits::is_set(decoderFlags, DecoderState::Flags::decodingStarted)) { - // Drain the ring buffer since the decoder could have contributed stale frames - setFlags(Flags::drainRequired); + // Empty the ring buffer since the decoder could have contributed stale frames + setFlags(Flags::audioStale); // Increment the playback epoch to expire any inflight events playbackGeneration_.fetch_add(1, std::memory_order_release); } - decoderState->setFlags(DecoderState::Flags::isCanceled); + decoderState->setFlags(DecoderState::Flags::canceled); anyCanceled = true; // Submit the decoder canceled event @@ -1240,8 +1243,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re continue; } - // Mute until the seek is complete and the ring buffer is drained and refilled - setFlags(Flags::isMuted | Flags::drainRequired); + // Mute until the seek is complete and the ring buffer is emptied and refilled + setFlags(Flags::muted | Flags::audioStale); { // Ensure the playback epoch increment and frame counter updates occur together @@ -1286,7 +1289,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } const auto nextDecoderFlags = nextDecoderState->loadFlags(); - if (bits::is_set(nextDecoderFlags, DecoderState::Flags::isCanceled)) { + if (bits::is_set(nextDecoderFlags, DecoderState::Flags::canceled)) { continue; } @@ -1341,7 +1344,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re const auto iter = std::ranges::find_if(activeDecoders_, [](const auto &decoderState) noexcept { const auto decoderFlags = decoderState->loadFlags(); return bits::has_none(decoderFlags, - DecoderState::Flags::isCanceled | DecoderState::Flags::decodingComplete); + DecoderState::Flags::canceled | DecoderState::Flags::decodingComplete); }); decoderState = iter != activeDecoders_.cend() ? iter->get() : nullptr; @@ -1459,7 +1462,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re continue; } - clearFlags(Flags::drainRequired); + clearFlags(Flags::audioStale); formatMismatch = false; // Allocate the buffer that is the intermediary between the decoder state and the ring buffer @@ -1485,10 +1488,9 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } if (decoderState != nullptr) { - if (const auto flags = loadFlags(); bits::is_clear(flags, Flags::drainRequired)) { + if (const auto flags = loadFlags(); bits::is_clear(flags, Flags::audioStale)) { // Decode and write chunks and metadata to the ring buffers - while (audioBuffer_.freeSpace() >= ringBufferChunkSize && !audioMetadata_.isFull()) { - + while (audioBuffer_.availableToWrite() >= ringBufferChunkSize && !audioMetadata_.isFull()) { // The chunk descriptor for the chunk to be decoded detail::DecodedChunkDescriptor descriptor{}; descriptor.playbackGeneration_ = playbackGeneration_.load(std::memory_order_acquire); @@ -1534,7 +1536,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } // Write the decoded audio to the audio buffer for rendering - const auto framesWritten = audioBuffer_.write(buffer.audioBufferList, buffer.frameLength); + const auto framesWritten = audioBuffer_.write(*(buffer.audioBufferList), buffer.frameLength); if (framesWritten != buffer.frameLength) { os_log_fault(log_, "Error writing audio: spsc::AudioRingBuffer::write failed for %u frames", buffer.frameLength); @@ -1566,8 +1568,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } // Clear the mute flag if needed now that the ring buffer is full - if (bits::is_set(flags, Flags::isMuted)) { - clearFlags(Flags::isMuted); + if (bits::is_set(flags, Flags::muted)) { + clearFlags(Flags::muted); } } } @@ -1585,7 +1587,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Determine timeout based on ring buffer free space // Attempt to keep the ring buffer 75% full const auto targetMaxFreeSpace = audioBuffer_.capacity() / 4; - const auto freeSpace = audioBuffer_.freeSpace(); + const auto freeSpace = audioBuffer_.availableToWrite(); if (freeSpace > targetMaxFreeSpace) { // Minimal timeout if the ring buffer has more free space than desired @@ -1608,29 +1610,32 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // MARK: - Rendering OSStatus sfb::AudioPlayer::render(BOOL &isSilence, const AudioTimeStamp ×tamp, AVAudioFrameCount frameCount, - AudioBufferList *outputData) noexcept { + AudioBufferList &outputData) noexcept { const auto flags = loadFlags(); /// Sets the buffers in an AudioBufferList struct to zero. - const auto zeroABL = [](AudioBufferList *abl) noexcept { - for (UInt32 i = 0; i < abl->mNumberBuffers; ++i) { - std::memset(abl->mBuffers[i].mData, 0, abl->mBuffers[i].mDataByteSize); + const auto zeroABL = [](AudioBufferList &abl) noexcept { + for (UInt32 i = 0; i < abl.mNumberBuffers; ++i) { +#if DEBUG + assert(abl.mBuffers[i].mData != nullptr); +#endif + std::memset(abl.mBuffers[i].mData, 0, abl.mBuffers[i].mDataByteSize); } }; // Discard any stale frames in the ring buffer from a seek or decoder cancelation - if (bits::is_set(flags, Flags::drainRequired)) { - audioBuffer_.drain(); + if (bits::is_set(flags, Flags::audioStale)) { + audioBuffer_.discardAll(); audioMetadata_.discardAll(); renderingChunk_ = {}; - clearFlags(Flags::drainRequired); + clearFlags(Flags::audioStale); zeroABL(outputData); isSilence = YES; return noErr; } // Output silence if muted or not playing - if (bits::is_set_or_is_clear(flags, Flags::isMuted, Flags::isPlaying)) { + if (bits::is_set_or_is_clear(flags, Flags::muted, Flags::playing)) { zeroABL(outputData); isSilence = YES; return noErr; @@ -1791,7 +1796,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re [delegate audioPlayer:player_ decodingStarted:decoder]; } - if (bits::is_clear(loadFlags(), Flags::isPlaying) && decoder == currentDecoder) { + if (bits::is_clear(loadFlags(), Flags::playing) && decoder == currentDecoder) { setNowPlaying(decoder); } @@ -2264,7 +2269,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Cancel all active decoders auto signal = false; for (const auto &decoderState : activeDecoders_) { - if (bits::is_clear(decoderState->loadFlags(), DecoderState::Flags::isCanceled)) { + if (bits::is_clear(decoderState->loadFlags(), DecoderState::Flags::canceled)) { decoderState->setFlags(DecoderState::Flags::cancelRequested); signal = true; } @@ -2282,7 +2287,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re #endif /* DEBUG */ const auto iter = std::ranges::find_if(activeDecoders_, [](const auto &decoderState) noexcept { const auto decoderFlags = decoderState->loadFlags(); - return bits::has_none(decoderFlags, DecoderState::Flags::needsInitialization | DecoderState::Flags::isCanceled); + return bits::has_none(decoderFlags, DecoderState::Flags::needsInitialization | DecoderState::Flags::canceled); }); return iter != activeDecoders_.cend() ? iter->get() : nullptr; } @@ -2314,8 +2319,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // AVAudioEngine stops itself when a configuration change occurs // Flags::engineIsRunning indicates if the engine was running before the configuration change - const auto prevFlags = clearFlags(Flags::engineIsRunning | Flags::isPlaying); - const auto prevState = prevFlags & (Flags::engineIsRunning | Flags::isPlaying); + const auto prevFlags = clearFlags(Flags::engineRunning | Flags::playing); + const auto prevState = prevFlags & (Flags::engineRunning | Flags::playing); AVAudioOutputNode *outputNode = engine_.outputNode; AVAudioMixerNode *mixerNode = engine_.mainMixerNode; @@ -2351,7 +2356,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // so ensure the engine is stopped before updating the bus format. if (engine_.isRunning) { #if DEBUG - assert(bits::is_set(prevState, Flags::engineIsRunning)); + assert(bits::is_set(prevState, Flags::engineRunning)); #endif /* DEBUG */ [engine_ stop]; } @@ -2365,7 +2370,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } // Restart AVAudioEngine if previously running - if (bits::is_set(prevState, Flags::engineIsRunning)) { + if (bits::is_set(prevState, Flags::engineRunning)) { if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); lock.unlock(); @@ -2488,7 +2493,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re return false; } [engine_ stop]; - clearFlags(Flags::engineIsRunning | Flags::isPlaying); + clearFlags(Flags::engineRunning | Flags::playing); return true; } @@ -2523,8 +2528,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re [engine_ stop]; // Attempt to preserve the playback state - const auto prevFlags = clearFlags(Flags::engineIsRunning | Flags::isPlaying); - const auto prevState = prevFlags & (Flags::engineIsRunning | Flags::isPlaying); + const auto prevFlags = clearFlags(Flags::engineRunning | Flags::playing); + const auto prevState = prevFlags & (Flags::engineRunning | Flags::playing); // Reconfigure the processing graph AVAudioConnectionPoint *sourceNodeOutputConnectionPoint = [[engine_ outputConnectionPointsForNode:sourceNode_ @@ -2566,7 +2571,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re [engine_ prepare]; // Restart AVAudioEngine and playback as appropriate - if (bits::is_set(prevState, Flags::engineIsRunning)) { + if (bits::is_set(prevState, Flags::engineRunning)) { if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); if (error != nullptr) { From cb7940bcec81352ed71612bf0c367d0f9f6058b2 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Fri, 24 Jul 2026 19:07:06 -0500 Subject: [PATCH 18/20] Remove assertions --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index c5bbcb31..6d96903f 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -527,9 +527,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re sourceNode_ = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus(BOOL *isSilence, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, AudioBufferList *outputData) { -#if DEBUG - assert(outputData != nullptr); -#endif return render(*isSilence, *timestamp, frameCount, *outputData); }]; if (sourceNode_ == nil) { @@ -1616,9 +1613,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re /// Sets the buffers in an AudioBufferList struct to zero. const auto zeroABL = [](AudioBufferList &abl) noexcept { for (UInt32 i = 0; i < abl.mNumberBuffers; ++i) { -#if DEBUG - assert(abl.mBuffers[i].mData != nullptr); -#endif std::memset(abl.mBuffers[i].mData, 0, abl.mBuffers[i].mDataByteSize); } }; From 379d975275175d91638281808f5cea83d8e85949 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Fri, 24 Jul 2026 19:10:45 -0500 Subject: [PATCH 19/20] Update CXXQueue to version 0.1.1 --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 45d79473..7358eaae 100644 --- a/Package.resolved +++ b/Package.resolved @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sbooth/CXXQueue", "state" : { - "revision" : "7e4e21a78c4bde49d6b4f4ec4220dfbf81e8e1c5", - "version" : "0.1.0" + "revision" : "b3b39f38e81d1fc5294ae99c81e019e1fe3b8582", + "version" : "0.1.1" } }, { diff --git a/Package.swift b/Package.swift index 67777ddb..9132ffd2 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,7 @@ let package = Package( .package(url: "https://github.com/sbooth/CXXAudioRingBuffer", .upToNextMinor(from: "0.2.0")), .package(url: "https://github.com/sbooth/CXXDispatchSemaphore", .upToNextMinor(from: "0.4.1")), .package(url: "https://github.com/sbooth/CXXMessageQueue", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/sbooth/CXXQueue", .upToNextMinor(from: "0.1.0")), + .package(url: "https://github.com/sbooth/CXXQueue", .upToNextMinor(from: "0.1.1")), .package(url: "https://github.com/sbooth/CXXUnfairLock", .upToNextMinor(from: "0.3.1")), // Standalone dependencies from source From d276752a58132aa8720d8e16383273c4b1117abf Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Fri, 24 Jul 2026 19:22:37 -0500 Subject: [PATCH 20/20] Use relaxed load for playbackGeneration_ in decoding thread --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 6d96903f..063f9be8 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1490,7 +1490,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re while (audioBuffer_.availableToWrite() >= ringBufferChunkSize && !audioMetadata_.isFull()) { // The chunk descriptor for the chunk to be decoded detail::DecodedChunkDescriptor descriptor{}; - descriptor.playbackGeneration_ = playbackGeneration_.load(std::memory_order_acquire); + descriptor.playbackGeneration_ = playbackGeneration_.load(std::memory_order_relaxed); descriptor.sequenceNumber_ = decoderState->sequenceNumber_; // Decoding started