From 6eec2ea1f585dfbfdb6414fb8daba26527baa24d Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 12 Jul 2026 13:22:09 -0500 Subject: [PATCH 1/6] Suppress underrun events when a format change is pending --- Sources/CSFBAudioEngine/Player/AudioPlayer.h | 4 +++- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 15 ++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index dba628d6..b49175ba 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -207,8 +207,10 @@ class AudioPlayer final { isMuted = 1u << 2, /// The ring buffer needs to be drained during the next render cycle drainRequired = 1u << 3, + /// A ring buffer format change is pending + pendingFormatChange = 1u << 4, /// The event message queue had insufficient space to record a render event - renderEventDropped = 1u << 4, + renderEventDropped = 1u << 5, }; // Enable bitmask operations for `Flags` diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 5e33d3a3..38471b69 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1423,13 +1423,16 @@ 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_); auto renderFormat = decoderState->converter_.outputFormat; - if (NSError *error = nil; !configureProcessingGraphAndRingBufferForFormat(renderFormat, &error)) { + NSError *error = nil; + const auto reconfigured = configureProcessingGraphAndRingBufferForFormat(renderFormat, &error); + + clearFlags(Flags::drainRequired | Flags::pendingFormatChange); + formatMismatch = false; + + if (!reconfigured) { decoderState->error_ = error; decoderState->setFlags(DecoderState::Flags::cancelRequested); continue; @@ -1452,6 +1455,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } } } else { + setFlags(Flags::pendingFormatChange); decoderState = nullptr; } } @@ -1606,7 +1610,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re isSilence = YES; } - if (framesRead != frameCount) { + // A short frame count without a pending format change is unexpected + if (framesRead != frameCount && bits::is_clear(flags, Flags::pendingFormatChange)) { if (!events_.enqueue(EventCommand::renderBufferUnderrun, timestamp.mHostTime, static_cast(framesRead), static_cast(frameCount))) { setFlags(Flags::renderEventDropped); From 3df3047269462dfd291b046426aed29ab21e80ba Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 12 Jul 2026 13:29:05 -0500 Subject: [PATCH 2/6] Move `setFlags` --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 38471b69..a74ffe05 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1416,6 +1416,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // If there is a format mismatch the processing graph requires reconfiguration before decoding can begin if (formatMismatch) { + setFlags(Flags::pendingFormatChange); + // Wait until all other decoders complete processing before reconfiguring the graph const auto okToReconfigure = [&]() noexcept { std::lock_guard lock{activeDecodersMutex_}; @@ -1455,7 +1457,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } } } else { - setFlags(Flags::pendingFormatChange); decoderState = nullptr; } } From b995701de53e65b70a1b76e5c20781a033f1435d Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Mon, 13 Jul 2026 06:32:06 -0500 Subject: [PATCH 3/6] Update comment --- 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 a74ffe05..c4865ae7 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1611,7 +1611,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re isSilence = YES; } - // A short frame count without a pending format change is unexpected + // A short frame count without a pending format change is generally unexpected if (framesRead != frameCount && bits::is_clear(flags, Flags::pendingFormatChange)) { if (!events_.enqueue(EventCommand::renderBufferUnderrun, timestamp.mHostTime, static_cast(framesRead), static_cast(frameCount))) { From e9fea1b75cb73eaac1a95d46d56bec59c326aab9 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Tue, 14 Jul 2026 09:43:30 -0500 Subject: [PATCH 4/6] Move flag set and add explanatory comments --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index c838ea76..36db29d5 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1217,6 +1217,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Clear the format mismatch flag if any decoders were canceled if (anyCanceled && formatMismatch) { formatMismatch = false; + clearFlags(Flags::pendingFormatChange); } // Get the earliest decoder state that has not completed rendering @@ -1250,6 +1251,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // match. Clear the format mismatch flag so rendering can continue; the flag will be set again when // decoding completes. formatMismatch = false; + clearFlags(Flags::pendingFormatChange); fetchUpdate( decoderState->flags_, @@ -1418,13 +1420,15 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // decoding can't start until the processing graph is reconfigured which occurs after // all active decoders complete formatMismatch = true; + + // Ring buffer underruns are expected while waiting for the format change to complete; + // suppress underrun notifications until the processing graph is reconfigured + setFlags(Flags::pendingFormatChange); } } // If there is a format mismatch the processing graph requires reconfiguration before decoding can begin if (formatMismatch) { - setFlags(Flags::pendingFormatChange); - // Wait until all other decoders complete processing before reconfiguring the graph const auto okToReconfigure = [&]() noexcept { std::lock_guard lock{activeDecodersMutex_}; @@ -1438,8 +1442,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re NSError *error = nil; const auto reconfigured = configureProcessingGraphAndRingBufferForFormat(renderFormat, &error); - clearFlags(Flags::drainRequired | Flags::pendingFormatChange); formatMismatch = false; + clearFlags(Flags::drainRequired | Flags::pendingFormatChange); if (!reconfigured) { decoderState->error_ = error; @@ -1618,7 +1622,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re isSilence = YES; } - // A short frame count without a pending format change is generally unexpected + // Suppress underrun notifications while a non-gapless format change is pending; the ring buffer + // is expected to run dry while the decoding thread waits to reconfigure the processing graph if (framesRead != frameCount && bits::is_clear(flags, Flags::pendingFormatChange)) { if (!events_.enqueue(EventCommand::renderBufferUnderrun, timestamp.mHostTime, static_cast(framesRead), static_cast(frameCount))) { From 500a9e69395ff158150472144c62c521e9f36550 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Tue, 14 Jul 2026 09:44:34 -0500 Subject: [PATCH 5/6] Rename flag --- Sources/CSFBAudioEngine/Player/AudioPlayer.h | 2 +- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index b49175ba..e8955a03 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -208,7 +208,7 @@ class AudioPlayer final { /// The ring buffer needs to be drained during the next render cycle drainRequired = 1u << 3, /// A ring buffer format change is pending - pendingFormatChange = 1u << 4, + formatChangePending = 1u << 4, /// The event message queue had insufficient space to record a render event renderEventDropped = 1u << 5, }; diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 36db29d5..90af4047 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1217,7 +1217,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Clear the format mismatch flag if any decoders were canceled if (anyCanceled && formatMismatch) { formatMismatch = false; - clearFlags(Flags::pendingFormatChange); + clearFlags(Flags::formatChangePending); } // Get the earliest decoder state that has not completed rendering @@ -1251,7 +1251,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // match. Clear the format mismatch flag so rendering can continue; the flag will be set again when // decoding completes. formatMismatch = false; - clearFlags(Flags::pendingFormatChange); + clearFlags(Flags::formatChangePending); fetchUpdate( decoderState->flags_, @@ -1423,7 +1423,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Ring buffer underruns are expected while waiting for the format change to complete; // suppress underrun notifications until the processing graph is reconfigured - setFlags(Flags::pendingFormatChange); + setFlags(Flags::formatChangePending); } } @@ -1443,7 +1443,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re const auto reconfigured = configureProcessingGraphAndRingBufferForFormat(renderFormat, &error); formatMismatch = false; - clearFlags(Flags::drainRequired | Flags::pendingFormatChange); + clearFlags(Flags::drainRequired | Flags::formatChangePending); if (!reconfigured) { decoderState->error_ = error; @@ -1624,7 +1624,7 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // Suppress underrun notifications while a non-gapless format change is pending; the ring buffer // is expected to run dry while the decoding thread waits to reconfigure the processing graph - if (framesRead != frameCount && bits::is_clear(flags, Flags::pendingFormatChange)) { + if (framesRead != frameCount && bits::is_clear(flags, Flags::formatChangePending)) { if (!events_.enqueue(EventCommand::renderBufferUnderrun, timestamp.mHostTime, static_cast(framesRead), static_cast(frameCount))) { setFlags(Flags::renderEventDropped); From 2bc5752dfe3aa9a74fc9abe17812a291993ef921 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sat, 25 Jul 2026 08:24:26 -0500 Subject: [PATCH 6/6] Use consistent assignment order --- 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 cbc8ca2d..2bfca12f 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -1465,8 +1465,8 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re continue; } - clearFlags(Flags::audioStale | Flags::formatChangePending); formatMismatch = false; + clearFlags(Flags::audioStale | Flags::formatChangePending); // Allocate the buffer that is the intermediary between the decoder state and the ring buffer if (auto format = buffer.format; format.channelCount != renderFormat.channelCount ||