diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index 1847ea03..8b27931e 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -148,7 +148,7 @@ class AudioPlayer final { dsema::Semaphore eventSemaphore_{0}; /// Message queue communicating events to the event processing thread - mpsc::MessageQueue<256, 40> events_; + mpsc::MessageQueue<256, 32> events_; /// The `AVAudioEngine` instance AVAudioEngine *engine_{nil}; @@ -391,10 +391,14 @@ class AudioPlayer final { decoderCanceled = 4, /// Allocation failure allocationFailure = 5, + /// Rendering started + renderingStarted = 6, /// Audio frames rendered from ring buffer - framesRendered = 6, + framesRendered = 7, + /// Rendering complete + renderingComplete = 8, /// Ring buffer contained fewer audio frames than requested - renderBufferUnderrun = 7, + renderBufferUnderrun = 9, }; // MARK: - Event Processing @@ -418,9 +422,15 @@ class AudioPlayer final { /// Dequeues and processes an allocation failure event from `events_` bool processAllocationFailureEvent() noexcept; + /// Dequeues and processes a rendering started event from `events_` + bool processRenderingStartedEvent() noexcept; + /// Dequeues and processes a frames rendered event from `events_` bool processFramesRenderedEvent() noexcept; + /// Dequeues and processes a rendering complete event from `events_` + bool processRenderingCompleteEvent() noexcept; + /// Reads and processes a render buffer underrun event from `events_` bool processRenderBufferUnderrunEvent() noexcept; diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 5a7696ea..93ebbc2b 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -179,18 +179,6 @@ T fetchUpdate(std::atomic &atom, Func &&func, /// Returns the absolute difference between a and b constexpr uint64_t absoluteDifference(uint64_t a, uint64_t b) noexcept { return (a >= b) ? (a - b) : (b - a); } -/// Possible flag bits for a frames rendered event -enum class FramesRenderedEventFlags : uint16_t { - /// Clear - none = 0, - /// Rendering starting - starting = 1u << 0, - /// Rendering complete - complete = 1u << 1, -}; - -constexpr void is_bitmask_enum(FramesRenderedEventFlags); - /// Hints to the CPU that the current thread is in a spin-wait loop. /// /// On supported architectures this emits the processor's spin-wait hint @@ -1654,6 +1642,10 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re } void sfb::AudioPlayer::enqueueFramesRenderedEvents(uint32_t framesRead, const AudioTimeStamp ×tamp) noexcept { +#if DEBUG + assert(framesRead > 0); +#endif /* DEBUG */ + auto framesRemaining = framesRead; do { // Read the next chunk descriptor if needed @@ -1666,33 +1658,49 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re renderingChunk_.emplace(chunkDescriptor); } + const auto frameOffset = framesRead - framesRemaining; + const auto chunkSequenceNumber = renderingChunk_->descriptor_.sequenceNumber_; const auto chunkFramesRemaining = renderingChunk_->framesRemaining(); - const auto framesFromChunk = std::min(chunkFramesRemaining, framesRemaining); + const auto chunkFramesConsumed = std::min(chunkFramesRemaining, framesRemaining); - const auto frameOffset = framesRead - framesRemaining; - const auto sampleRate = audioBuffer_.format().mSampleRate; - - const auto isStart = - renderingChunk_->framesConsumed_ == 0 && chunkSequenceNumber != lastRenderedSequenceNumber_; - const auto startTime = isStart ? hostTimeForFrameOffset(frameOffset, timestamp, sampleRate) : 0; +#if DEBUG + assert(chunkFramesConsumed > 0 || renderingChunk_->descriptor_.isLast_); +#endif /* DEBUG */ - const auto isEnd = renderingChunk_->descriptor_.isLast_ && framesFromChunk == chunkFramesRemaining; - const auto endTime = isEnd ? hostTimeForFrameOffset(frameOffset + framesFromChunk, timestamp, sampleRate) : 0; + // Rendering is starting + if (renderingChunk_->framesConsumed_ == 0 && chunkSequenceNumber != lastRenderedSequenceNumber_) [[unlikely]] { + const auto eventTime = hostTimeForFrameOffset(frameOffset, timestamp, audioBuffer_.format().mSampleRate); + if (!events_.enqueue(EventCommand::renderingStarted, eventTime, + renderingChunk_->descriptor_.sequenceNumber_, + renderingChunk_->descriptor_.playbackGeneration_)) [[unlikely]] { + setFlags(Flags::renderEventDropped); + } + } - const auto eventFlags = (isStart ? FramesRenderedEventFlags::starting : FramesRenderedEventFlags::none) | - (isEnd ? FramesRenderedEventFlags::complete : FramesRenderedEventFlags::none); + // Submit the frames rendered event if any frames were rendered + if (chunkFramesConsumed > 0) [[likely]] { + if (!events_.enqueue(EventCommand::framesRendered, renderingChunk_->descriptor_.sequenceNumber_, + chunkFramesConsumed, renderingChunk_->descriptor_.playbackGeneration_)) [[unlikely]] { + setFlags(Flags::renderEventDropped); + } + } - // Enqueue the frames rendered event - if (!events_.enqueue(EventCommand::framesRendered, startTime, endTime, chunkSequenceNumber, framesFromChunk, - renderingChunk_->descriptor_.playbackGeneration_, eventFlags)) [[unlikely]] { - setFlags(Flags::renderEventDropped); + // Rendering is complete + if (renderingChunk_->descriptor_.isLast_ && chunkFramesConsumed == chunkFramesRemaining) [[unlikely]] { + const auto eventTime = hostTimeForFrameOffset(frameOffset + chunkFramesConsumed, timestamp, + audioBuffer_.format().mSampleRate); + if (!events_.enqueue(EventCommand::renderingComplete, eventTime, + renderingChunk_->descriptor_.sequenceNumber_, + renderingChunk_->descriptor_.playbackGeneration_)) [[unlikely]] { + setFlags(Flags::renderEventDropped); + } } // Accounting lastRenderedSequenceNumber_ = chunkSequenceNumber; - renderingChunk_->framesConsumed_ += framesFromChunk; - framesRemaining -= framesFromChunk; + renderingChunk_->framesConsumed_ += chunkFramesConsumed; + framesRemaining -= chunkFramesConsumed; // Chunk processing complete if (renderingChunk_->allFramesConsumed()) { @@ -1716,13 +1724,18 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re const auto chunkSequenceNumber = chunkDescriptor.sequenceNumber_; const auto eventTime = hostTimeForFrameOffset(0, timestamp, audioBuffer_.format().mSampleRate); - const auto eventFlags = chunkSequenceNumber != lastRenderedSequenceNumber_ - ? (FramesRenderedEventFlags::starting | FramesRenderedEventFlags::complete) - : FramesRenderedEventFlags::complete; - // Enqueue the empty frames rendered event - if (!events_.enqueue(EventCommand::framesRendered, eventTime, eventTime, chunkSequenceNumber, - static_cast(0), chunkDescriptor.playbackGeneration_, eventFlags)) [[unlikely]] { + // Rendering is starting + if (chunkSequenceNumber != lastRenderedSequenceNumber_) { + if (!events_.enqueue(EventCommand::renderingStarted, eventTime, chunkDescriptor.sequenceNumber_, + chunkDescriptor.playbackGeneration_)) [[unlikely]] { + setFlags(Flags::renderEventDropped); + } + } + + // Rendering is complete + if (!events_.enqueue(EventCommand::renderingComplete, eventTime, chunkDescriptor.sequenceNumber_, + chunkDescriptor.playbackGeneration_)) [[unlikely]] { setFlags(Flags::renderEventDropped); } @@ -1763,9 +1776,15 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re case EventCommand::allocationFailure: processAllocationFailureEvent(); break; + case EventCommand::renderingStarted: + processRenderingStartedEvent(); + break; case EventCommand::framesRendered: processFramesRenderedEvent(); break; + case EventCommand::renderingComplete: + processRenderingCompleteEvent(); + break; case EventCommand::renderBufferUnderrun: processRenderBufferUnderrunEvent(); break; @@ -2031,22 +2050,73 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // MARK: Rendering Events +bool sfb::AudioPlayer::processRenderingStartedEvent() noexcept { + EventCommand command; + // The event time calculated from the render cycle's host time and rate scalar + uint64_t eventTime; + // The decoder sequence number for the decoder + uint64_t sequenceNumber; + // The playback generation of the chunk containing the frames + uint64_t playbackGeneration; + + if (!events_.dequeue(command, eventTime, sequenceNumber, playbackGeneration)) { + os_log_error(log_, + "Missing event time, decoder sequence number, or playback generation for rendering started event"); + return false; + } + +#if DEBUG + assert(command == EventCommand::renderingStarted); +#endif /* DEBUG */ + + Decoder decoder{nil}; + + { + 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 rendering started event"); + return true; + } + + if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); + iter != activeDecoders_.cend()) { +#if DEBUG + const auto decoderFlags = (*iter)->loadFlags(); + assert(bits::is_set(decoderFlags, DecoderState::Flags::decodingStarted)); + assert(bits::is_clear(decoderFlags, DecoderState::Flags::renderingStarted)); +#endif /* DEBUG */ + + (*iter)->setFlags(DecoderState::Flags::renderingStarted); + publishTransportSnapshot((*iter)->snapshot()); + decoder = (*iter)->decoder_; + } else { + os_log_error(log_, "Decoder state with sequence number %llu missing for rendering started event", + sequenceNumber); + return false; + } + } + +#if DEBUG + assert(decoder != nil); +#endif /* DEBUG */ + + handleRenderingWillStartEvent(decoder, eventTime); + return true; +} + bool sfb::AudioPlayer::processFramesRenderedEvent() noexcept { EventCommand command; - // The starting and ending host time for the frames - uint64_t startTime; - uint64_t endTime; // The decoder sequence number for the decoder providing the frames uint64_t sequenceNumber; // The number of valid frames rendered uint32_t frameCount; // The playback generation of the chunk containing the frames uint64_t playbackGeneration; - // Event flags - FramesRenderedEventFlags eventFlags; - if (!events_.dequeue(command, startTime, endTime, sequenceNumber, frameCount, playbackGeneration, eventFlags)) { - os_log_error(log_, "Missing start time, end time, decoder sequence number, frame count, playback generation, " - "or flags for frames rendered event"); + if (!events_.dequeue(command, sequenceNumber, frameCount, playbackGeneration)) { + os_log_error(log_, + "Missing decoder sequence number, frame count, or playback generation for frames rendered event"); return false; } @@ -2069,15 +2139,6 @@ Flags clearFlags(Flags flags, std::memory_order order = std::memory_order_acq_re // the seek's reset can land between the check and the read below, making a stale event's // frame count get applied to post-seek counters. - struct RenderingEventDetails { - Decoder decoder_{nil}; - uint64_t time_{0}; - }; - - // Events to be dispatched once the lock is released - std::optional willStartEvent; - std::optional willCompleteEvent; - { std::lock_guard lock{activeDecodersMutex_}; @@ -2089,62 +2150,93 @@ 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()) { +#if DEBUG + assert(frameCount > 0); const auto decoderFlags = (*iter)->loadFlags(); + assert(bits::is_set(decoderFlags, DecoderState::Flags::decodingStarted)); + assert(bits::is_set(decoderFlags, DecoderState::Flags::renderingStarted)); +#endif /* DEBUG */ + + (*iter)->framesRendered_.fetch_add(frameCount, std::memory_order_acq_rel); + publishTransportSnapshot((*iter)->snapshot()); + } else { + os_log_error(log_, "Decoder state with sequence number %llu missing for frames rendered event", + sequenceNumber); + return false; + } + } + + return true; +} + +bool sfb::AudioPlayer::processRenderingCompleteEvent() noexcept { + EventCommand command; + // The event time calculated from the render cycle's host time and rate scalar + uint64_t eventTime; + // The decoder sequence number for the decoder + uint64_t sequenceNumber; + // The playback generation of the chunk containing the frames + uint64_t playbackGeneration; + + if (!events_.dequeue(command, eventTime, sequenceNumber, playbackGeneration)) { + os_log_error( + log_, + "Missing event time, decoder sequence number, or playback generation for rendering complete event"); + return false; + } - // Rendering is starting - if (bits::is_set(eventFlags, FramesRenderedEventFlags::starting)) [[unlikely]] { #if DEBUG - assert(bits::is_clear(decoderFlags, DecoderState::Flags::renderingStarted)); + assert(command == EventCommand::renderingComplete); #endif /* DEBUG */ - (*iter)->setFlags(DecoderState::Flags::renderingStarted); - willStartEvent.emplace((*iter)->decoder_, startTime); - } - if (frameCount > 0) [[likely]] { - (*iter)->framesRendered_.fetch_add(frameCount, std::memory_order_acq_rel); - } + Decoder decoder{nil}; + + { + 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 rendering complete event"); + return true; + } - // Rendering is complete - if (bits::is_set(eventFlags, FramesRenderedEventFlags::complete)) [[unlikely]] { + if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); + iter != activeDecoders_.cend()) { #if DEBUG - assert(bits::is_set(decoderFlags, DecoderState::Flags::decodingStarted)); - assert(bits::is_set(decoderFlags, DecoderState::Flags::renderingStarted)); - assert(bits::is_set(decoderFlags, DecoderState::Flags::decodingComplete)); + const auto decoderFlags = (*iter)->loadFlags(); + assert(bits::is_set(decoderFlags, DecoderState::Flags::decodingStarted)); + assert(bits::is_set(decoderFlags, DecoderState::Flags::renderingStarted)); + assert(bits::is_set(decoderFlags, DecoderState::Flags::decodingComplete)); #endif /*DEBUG */ - willCompleteEvent.emplace((*iter)->decoder_, endTime); - os_log_debug(log_, "Deleting decoder state for %{public}@", (*iter)->decoder_); - activeDecoders_.erase(iter); + decoder = (*iter)->decoder_; - // Wake the decoding thread if a format change is pending - if (activeDecoders_.size() == 1 && bits::is_set(loadFlags(), Flags::formatChangePending)) { - decodingSemaphore_.signal(); - } + os_log_debug(log_, "Deleting decoder state for %{public}@", (*iter)->decoder_); + activeDecoders_.erase(iter); - // Publish snapshot reflecting the new active decoder state - if (const auto *nextDecoderState = firstActiveDecoderState(); nextDecoderState != nullptr) { - publishTransportSnapshot(nextDecoderState->snapshot()); - } else { - publishTransportSnapshot({}); - } + // Wake the decoding thread if a format change is pending + if (activeDecoders_.size() == 1 && bits::is_set(loadFlags(), Flags::formatChangePending)) { + decodingSemaphore_.signal(); + } + + // Publish snapshot reflecting the new active decoder state + if (const auto *nextDecoderState = firstActiveDecoderState(); nextDecoderState != nullptr) { + publishTransportSnapshot(nextDecoderState->snapshot()); } else { - publishTransportSnapshot((*iter)->snapshot()); + publishTransportSnapshot({}); } } else { - os_log_error(log_, "Decoder state with sequence number %llu missing for frames rendered event", + os_log_error(log_, "Decoder state with sequence number %llu missing for rendering complete event", sequenceNumber); return false; } } - // Call functions that notify the delegate after unlocking the lock - if (willStartEvent) [[unlikely]] { - handleRenderingWillStartEvent(willStartEvent->decoder_, willStartEvent->time_); - } - if (willCompleteEvent) [[unlikely]] { - handleRenderingWillCompleteEvent(willCompleteEvent->decoder_, willCompleteEvent->time_); - } +#if DEBUG + assert(decoder != nil); +#endif /* DEBUG */ + handleRenderingWillCompleteEvent(decoder, eventTime); return true; }