From 9bdc01abfb9d12c66ec786a70f8448dd6fc6c133 Mon Sep 17 00:00:00 2001 From: "Stephen F. Booth" Date: Mon, 1 Dec 2025 08:42:21 -0600 Subject: [PATCH 1/9] Make `jthread` support conditional on C++ `201911L` --- .../CSFBAudioEngine/Player/AudioPlayerNode.h | 24 ++++++++++ .../CSFBAudioEngine/Player/AudioPlayerNode.mm | 45 +++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h index 4ca7eaa15..6498a0f74 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h @@ -10,7 +10,9 @@ #import #import #import +#if __has_include() #import +#endif /* __has_include() */ #import #import @@ -82,12 +84,20 @@ class AudioPlayerNode final { mutable CXXUnfairLock::UnfairLock queueLock_; /// Thread used for decoding +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L std::jthread decodingThread_; +#else + std::thread decodingThread_; +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ /// Dispatch semaphore used for communication with the decoding thread dispatch_semaphore_t decodingSemaphore_ {}; /// Thread used for event processing +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L std::jthread eventThread_; +#else + std::thread eventThread_; +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ /// Dispatch semaphore used for communication with the event processing thread dispatch_semaphore_t eventSemaphore_ {}; @@ -214,13 +224,23 @@ class AudioPlayerNode final { unmuteAfterDequeue = 1u << 3, /// The audio ring buffer requires a non-threadsafe reset ringBufferNeedsReset = 1u << 4, +#if !(defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L) + /// The decoding thread should exit + stopDecodingThread = 1u << 5, + /// The event thread should exit + stopEventThread = 1u << 6, +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ }; // MARK: - Decoding /// Dequeues and processes decoders from the decoder queue /// - note: This is the thread entry point for the decoding thread +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L void ProcessDecoders(std::stop_token stoken) noexcept; +#else + void ProcessDecoders() noexcept; +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ /// Writes an error event to `decodeEventRingBuffer_` and signals `eventSemaphore_` void SubmitDecodingErrorEvent(NSError *error) noexcept; @@ -282,7 +302,11 @@ class AudioPlayerNode final { /// Sequences events from from `decodeEventRingBuffer_` and `renderEventRingBuffer_` for processing in order /// - note: This is the thread entry point for the event thread +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L void SequenceAndProcessEvents(std::stop_token stoken) noexcept; +#else + void SequenceAndProcessEvents() noexcept; +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ /// Processes an event from `decodeEventRingBuffer_` void ProcessDecodingEvent(const DecodingEventHeader& header) noexcept; diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm index d4de8e2c6..a7ba74f7d 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm @@ -324,8 +324,13 @@ bool PerformSeekIfRequired() noexcept // Launch the decoding and event processing threads try { +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L decodingThread_ = std::jthread(std::bind_front(&SFB::AudioPlayerNode::ProcessDecoders, this)); eventThread_ = std::jthread(std::bind_front(&SFB::AudioPlayerNode::SequenceAndProcessEvents, this)); +#else + decodingThread_ = std::thread(&SFB::AudioPlayerNode::ProcessDecoders, this); + eventThread_ = std::thread(&SFB::AudioPlayerNode::SequenceAndProcessEvents, this); +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ } catch(const std::exception& e) { os_log_error(log_, "Unable to create thread: %{public}s", e.what()); throw; @@ -336,6 +341,7 @@ bool PerformSeekIfRequired() noexcept { Stop(); +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L // Register a stop callback for the decoding thread std::stop_callback decodingThreadStopCallback(decodingThread_.get_stop_token(), [this] { dispatch_semaphore_signal(decodingSemaphore_); @@ -343,12 +349,19 @@ bool PerformSeekIfRequired() noexcept // Issue a stop request to the decoding thread and wait for it to exit decodingThread_.request_stop(); +#else + // Stop the decoding thread + flags_.fetch_or(static_cast(Flags::stopDecodingThread), std::memory_order_acq_rel); + dispatch_semaphore_signal(decodingSemaphore_); +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ + try { decodingThread_.join(); } catch(const std::exception& e) { os_log_error(log_, "Unable to join decoding thread: %{public}s", e.what()); } +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L // Register a stop callback for the event processing thread std::stop_callback eventThreadStopCallback(eventThread_.get_stop_token(), [this] { dispatch_semaphore_signal(eventSemaphore_); @@ -356,6 +369,12 @@ bool PerformSeekIfRequired() noexcept // Issue a stop request to the event processing thread and wait for it to exit eventThread_.request_stop(); +#else + // Stop the decoding thread + flags_.fetch_or(static_cast(Flags::stopEventThread), std::memory_order_acq_rel); + dispatch_semaphore_signal(eventSemaphore_); +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ + try { eventThread_.join(); } catch(const std::exception& e) { @@ -678,7 +697,11 @@ bool PerformSeekIfRequired() noexcept // MARK: - Decoding +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L void SFB::AudioPlayerNode::ProcessDecoders(std::stop_token stoken) noexcept +#else +void SFB::AudioPlayerNode::ProcessDecoders() noexcept +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ { pthread_setname_np("AudioPlayerNode.Decoding"); pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); @@ -722,8 +745,14 @@ bool PerformSeekIfRequired() noexcept } // Terminate the thread if requested after processing cancelations - if(stoken.stop_requested()) - break; + if( +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L + stoken.stop_requested() +#else + flags_.load(std::memory_order_acquire) & static_cast(Flags::stopDecodingThread) +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ + ) + break; // Process pending seeks if(decoderState && decoderState->IsSeekPending()) { @@ -1021,14 +1050,24 @@ bool PerformSeekIfRequired() noexcept // MARK: - Event Processing +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L void SFB::AudioPlayerNode::SequenceAndProcessEvents(std::stop_token stoken) noexcept +#else +void SFB::AudioPlayerNode::SequenceAndProcessEvents() noexcept +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ { pthread_setname_np("AudioPlayerNode.Events"); pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); os_log_debug(log_, "Event processing thread starting"); - while(!stoken.stop_requested()) { + while( +#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L + !stoken.stop_requested() +#else + !(flags_.load(std::memory_order_acquire) & static_cast(Flags::stopEventThread)) +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ + ) { auto decodeEventHeader = decodeEventRingBuffer_.ReadValue(); auto renderEventHeader = renderEventRingBuffer_.ReadValue(); From 37a14158223ab3b8023068f856ba278ec65b45df Mon Sep 17 00:00:00 2001 From: "Stephen F. Booth" Date: Mon, 1 Dec 2025 08:49:36 -0600 Subject: [PATCH 2/9] Fix comment --- Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm index a7ba74f7d..9f55388fb 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.mm @@ -370,7 +370,7 @@ bool PerformSeekIfRequired() noexcept // Issue a stop request to the event processing thread and wait for it to exit eventThread_.request_stop(); #else - // Stop the decoding thread + // Stop the event processing thread flags_.fetch_or(static_cast(Flags::stopEventThread), std::memory_order_acq_rel); dispatch_semaphore_signal(eventSemaphore_); #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ From 562284a9af813d688d4b63cddefd5e14745cff74 Mon Sep 17 00:00:00 2001 From: "Stephen F. Booth" Date: Mon, 1 Dec 2025 08:53:42 -0600 Subject: [PATCH 3/9] Check `defined(__has_include)` --- Sources/CSFBAudioEngine/Player/AudioPlayerNode.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h index 6498a0f74..3733774e3 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h @@ -10,9 +10,9 @@ #import #import #import -#if __has_include() +#if defined(__has_include) && __has_include() #import -#endif /* __has_include() */ +#endif /* defined(__has_include) && __has_include() */ #import #import From a7b8219ddbce4057b6312991e128d4276bcd0cb6 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Mon, 22 Dec 2025 18:34:04 -0600 Subject: [PATCH 4/9] Remove flag --- Sources/CSFBAudioEngine/Player/AudioPlayerNode.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h index d28160208..a6a24e342 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayerNode.h @@ -224,13 +224,11 @@ class AudioPlayerNode final { isMuted = 1u << 2, /// The decoding thread should unmute after the next decoder is dequeued and becomes active unmuteAfterDequeue = 1u << 3, - /// The audio ring buffer requires a non-threadsafe reset - ringBufferNeedsReset = 1u << 4, #if !(defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L) /// The decoding thread should exit - stopDecodingThread = 1u << 5, + stopDecodingThread = 1u << 4, /// The event thread should exit - stopEventThread = 1u << 6, + stopEventThread = 1u << 5, #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ }; From 10c9b49fe8c4898bd2226e1095fe434ee709394a Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Fri, 9 Jan 2026 09:20:36 -0600 Subject: [PATCH 5/9] Correct merge error --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index a11be8fc8..c589f7788 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -974,7 +974,6 @@ T fetch_update(std::atomic& atom, Func&& func, std::memory_order order = std: #else !(flags_.load(std::memory_order_acquire) & static_cast(Flags::stopDecodingThread)) #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - !stoken.stop_requested() ) { // The decoder state being processed DecoderState *decoderState = nullptr; From 78c16c6a9ca6ba86b7700b26b2e659c74ff0f9c4 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Fri, 9 Jan 2026 09:29:25 -0600 Subject: [PATCH 6/9] Refactor exit checks into a lambda --- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index c589f7788..8902dd23d 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -968,13 +968,16 @@ T fetch_update(std::atomic& atom, Func&& func, std::memory_order order = std: // Whether there is a mismatch between the rendering format and the next decoder's processing format auto formatMismatch = false; - while( + // Returns true if the decoding thread should exit + const auto stop_requested = [&] { #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - !stoken.stop_requested() + return stoken.stop_requested(); #else - !(flags_.load(std::memory_order_acquire) & static_cast(Flags::stopDecodingThread)) + return (flags_.load(std::memory_order_acquire) & static_cast(Flags::stopDecodingThread)); #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - ) { + }; + + while(!stop_requested()) { // The decoder state being processed DecoderState *decoderState = nullptr; auto ringBufferStale = false; @@ -1380,13 +1383,16 @@ T fetch_update(std::atomic& atom, Func&& func, std::memory_order order = std: os_log_debug(log_, " event processing thread starting", this); - while( + // Returns true if the event processing thread should exit + const auto stop_requested = [&] { #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - !stoken.stop_requested() + return stoken.stop_requested(); #else - !(flags_.load(std::memory_order_acquire) & static_cast(Flags::stopEventThread)) + return (flags_.load(std::memory_order_acquire) & static_cast(Flags::stopEventThread)); #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - ) { + }; + + while(!stop_requested()) { DecodingEventCommand decodingEventCommand; uint64_t decodingEventIdentificationNumber; auto gotDecodingEvent = decodingEvents_.ReadValues(decodingEventCommand, decodingEventIdentificationNumber); From db00e61b006415d6d6d728e7f857d97fd2cb5d3c Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Sun, 18 Jan 2026 10:59:19 -0600 Subject: [PATCH 7/9] Add closing condition comment --- Sources/CSFBAudioEngine/Player/AudioPlayer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index 004ffafe1..ea0bc49c5 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -226,7 +226,7 @@ class AudioPlayer final { void processDecoders(std::stop_token stoken) noexcept; #else void processDecoders() noexcept; -#endif +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ /// Writes an error event to `decodingEvents_` and signals `eventSemaphore_` void submitDecodingErrorEvent(NSError *error) noexcept; @@ -264,7 +264,7 @@ class AudioPlayer final { void sequenceAndProcessEvents(std::stop_token stoken) noexcept; #else void sequenceAndProcessEvents() noexcept; -#endif +#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ /// Reads and processes an event payload from `decodingEvents_` bool processDecodingEvent(DecodingEventCommand command) noexcept; From daca92c9fa4202cfc3b0e5356fb25134e3f147e5 Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Thu, 22 Jan 2026 11:11:27 -0600 Subject: [PATCH 8/9] Remove erroneous commit --- .../CSFBAudioEngine/Player/AudioPlayer.h.orig | 422 --- .../Player/AudioPlayer.mm.orig | 2360 ----------------- 2 files changed, 2782 deletions(-) delete mode 100644 Sources/CSFBAudioEngine/Player/AudioPlayer.h.orig delete mode 100644 Sources/CSFBAudioEngine/Player/AudioPlayer.mm.orig diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h.orig b/Sources/CSFBAudioEngine/Player/AudioPlayer.h.orig deleted file mode 100644 index 79df52d47..000000000 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h.orig +++ /dev/null @@ -1,422 +0,0 @@ -// -// Copyright (c) 2006-2026 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#pragma once - -#import "SFBAudioDecoder.h" -#import "SFBAudioPlayer.h" - -#import -#import -#import - -#import - -#import - -#import -#import -#import -#import -#import -#if defined(__has_include) && __has_include() -#import -#endif /* defined(__has_include) && __has_include() */ -#import -#import - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wnullability-completeness" - -namespace sfb { - -// MARK: - AudioPlayer - -/// SFBAudioPlayer implementation -class AudioPlayer final { - public: - using unique_ptr = std::unique_ptr; - using Decoder = id; - - /// The shared log for all `AudioPlayer` instances - static const os_log_t log_; - - /// Weak reference to owning `SFBAudioPlayer` instance - __weak SFBAudioPlayer *player_{nil}; - - private: - struct DecoderState; - - using DecoderStateVector = std::vector>; - - /// Ring buffer transferring audio between the decoding thread and the render block - CXXCoreAudio::AudioRingBuffer audioRingBuffer_; - - /// Active decoders and associated state - DecoderStateVector activeDecoders_; - /// Lock protecting `activeDecoders_` - mutable CXXUnfairLock::UnfairLock activeDecodersLock_; - - /// Decoders enqueued for playback that are not yet active - std::deque queuedDecoders_; - /// Lock protecting `queuedDecoders_` - mutable CXXUnfairLock::UnfairLock queuedDecodersLock_; - - /// Thread used for decoding -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - std::jthread decodingThread_; -#else - std::thread decodingThread_; -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - /// Dispatch semaphore used for communication with the decoding thread - dispatch_semaphore_t decodingSemaphore_ {nil}; - - /// Thread used for event processing -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - std::jthread eventThread_; -#else - std::thread eventThread_; -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - /// Dispatch semaphore used for communication with the event processing thread - dispatch_semaphore_t eventSemaphore_ {nil}; - - /// Ring buffer communicating events from the decoding thread to the event processing thread - CXXRingBuffer::RingBuffer decodingEvents_; - /// Ring buffer communicating events from the render block to the event processing thread - CXXRingBuffer::RingBuffer renderingEvents_; - - /// The `AVAudioEngine` instance - AVAudioEngine *engine_{nil}; - /// Source node driving the audio processing graph - AVAudioSourceNode *sourceNode_{nil}; - /// Lock protecting playback state and processing graph configuration changes - mutable CXXUnfairLock::UnfairLock engineLock_; - - /// Decoder currently rendering audio - Decoder nowPlaying_{nil}; - /// Lock protecting `nowPlaying_` - mutable CXXUnfairLock::UnfairLock nowPlayingLock_; - - /// Dispatch queue used for asynchronous render event notifications - dispatch_queue_t eventQueue_{nil}; - - /// Player flags - std::atomic_uint flags_{0}; - static_assert(std::atomic_uint::is_always_lock_free, "Lock-free std::atomic_uint required"); - -#if TARGET_OS_IPHONE - /// Playback state before audio session interruption - unsigned int preInterruptState_{0}; -#endif /* TARGET_OS_IPHONE */ - - public: - AudioPlayer(); - - AudioPlayer(const AudioPlayer&) = delete; - AudioPlayer& operator=(const AudioPlayer&) = delete; - - // AudioPlayer(AudioPlayer&&) = delete; - // AudioPlayer& operator=(AudioPlayer&&) = delete; - - ~AudioPlayer() noexcept; - - // MARK: - Playlist Management - - bool enqueueDecoder(Decoder _Nonnull decoder, bool forImmediatePlayback, NSError **error) noexcept; - - bool formatWillBeGaplessIfEnqueued(AVAudioFormat *_Nonnull format) const noexcept; - - void clearDecoderQueue() noexcept; - bool decoderQueueIsEmpty() const noexcept; - - // MARK: - Playback Control - - bool play(NSError **error) noexcept; - bool pause() noexcept; - bool resume() noexcept; - void stop() noexcept; - bool togglePlayPause(NSError **error) noexcept; - - void reset() noexcept; - - // MARK: - Player State - - bool engineIsRunning() const noexcept; - - SFBAudioPlayerPlaybackState playbackState() const noexcept; - - bool isPlaying() const noexcept; - bool isPaused() const noexcept; - bool isStopped() const noexcept; - bool isReady() const noexcept; - - Decoder _Nullable currentDecoder() const noexcept; - - Decoder _Nullable nowPlaying() const noexcept; - - private: - void setNowPlaying(Decoder _Nullable nowPlaying) noexcept; - - public: - // MARK: - Playback Properties - - SFBPlaybackPosition playbackPosition() const noexcept; - SFBPlaybackTime playbackTime() const noexcept; - bool getPlaybackPositionAndTime(SFBPlaybackPosition *_Nullable playbackPosition, - SFBPlaybackTime *_Nullable playbackTime) const noexcept; - - // MARK: - Seeking - - bool seekInTime(NSTimeInterval secondsToSkip) noexcept; - bool seekToTime(NSTimeInterval timeInSeconds) noexcept; - bool seekToPosition(double position) noexcept; - bool seekToFrame(AVAudioFramePosition frame) noexcept; - bool supportsSeeking() const noexcept; - -#if !TARGET_OS_IPHONE - // MARK: - Volume Control - - float volumeForChannel(AudioObjectPropertyElement channel) const noexcept; - bool setVolumeForChannel(float volume, AudioObjectPropertyElement channel, NSError **error) noexcept; - - // MARK: - Output Device - - AUAudioObjectID outputDeviceID() const noexcept; - bool setOutputDeviceID(AUAudioObjectID outputDeviceID, NSError **error) noexcept; -#endif /* !TARGET_OS_IPHONE */ - - // MARK: - AVAudioEngine - - void modifyProcessingGraph(void (^_Nonnull block)(AVAudioEngine *_Nonnull engine)) const noexcept; - - AVAudioSourceNode *_Nonnull sourceNode() const noexcept; - AVAudioMixerNode *_Nonnull mainMixerNode() const noexcept; - AVAudioOutputNode *_Nonnull outputNode() const noexcept; - - // MARK: - Debugging - - void logProcessingGraphDescription(os_log_t _Nonnull log, os_log_type_t type) const noexcept; - -<<<<<<< HEAD -private: - /// Possible bits in `flags_` - enum class Flags : unsigned int { - /// Cached value of `engine_.isRunning` - engineIsRunning = 1u << 0, - /// The render block should output audio - isPlaying = 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, -#if !(defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L) - /// The decoding thread should exit - stopDecodingThread = 1u << 4, - /// The event thread should exit - stopEventThread = 1u << 5, -#endif /* !(defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L) */ - }; -======= - private: - /// Possible bits in `flags_` - enum class Flags : unsigned int { - /// Cached value of `engine_.isRunning` - engineIsRunning = 1u << 0, - /// The render block should output audio - isPlaying = 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, - }; ->>>>>>> main - - // MARK: - Decoding - -<<<<<<< HEAD - /// Dequeues and processes decoders from the decoder queue - /// - note: This is the thread entry point for the decoding thread -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - void processDecoders(std::stop_token stoken) noexcept; -#else - void processDecoders() noexcept; -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ -======= - /// Dequeues and processes decoders from the decoder queue - /// - note: This is the thread entry point for the decoding thread - void processDecoders(std::stop_token stoken) noexcept; ->>>>>>> main - - /// Writes an error event to `decodingEvents_` and signals `eventSemaphore_` - void submitDecodingErrorEvent(NSError *error) noexcept; - - // MARK: - Rendering - - /// Render block implementation - OSStatus render(BOOL& isSilence, const AudioTimeStamp& timestamp, AVAudioFrameCount frameCount, - AudioBufferList *_Nonnull outputData) noexcept; - - // MARK: - Events - - /// Decoding thread events - enum class DecodingEventCommand : uint32_t { - /// Decoding started - started = 1, - /// Decoding complete - complete = 2, - /// Decoder canceled by user or aborted due to error - canceled = 3, - /// Decoding error - error = 4, - }; - - /// Render block events - enum class RenderingEventCommand : uint32_t { - /// Audio frames rendered from ring buffer - framesRendered = 1, - }; - - // MARK: - Event Processing - -<<<<<<< HEAD - /// Reads and sequences event headers from `decodingEvents_` and `renderingEvents_` for processing in order - /// - note: This is the thread entry point for the event processing thread -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - void sequenceAndProcessEvents(std::stop_token stoken) noexcept; -#else - void sequenceAndProcessEvents() noexcept; -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ -======= - /// Reads and sequences event headers from `decodingEvents_` and `renderingEvents_` for processing in order - /// - note: This is the thread entry point for the event processing thread - void sequenceAndProcessEvents(std::stop_token stoken) noexcept; ->>>>>>> main - - /// Reads and processes an event payload from `decodingEvents_` - bool processDecodingEvent(DecodingEventCommand command) noexcept; - - /// Reads and processes a decoding started event from `decodingEvents_` - bool processDecodingStartedEvent() noexcept; - - /// Reads and processes a decoding complete event from `decodingEvents_` - bool processDecodingCompleteEvent() noexcept; - - /// Reads and processes a decoder canceled event from `decodingEvents_` - bool processDecoderCanceledEvent() noexcept; - - /// Reads and processes a decoding error event from `decodingEvents_` - bool processDecodingErrorEvent() noexcept; - - /// Reads and processes an event payload from `renderingEvents_` - bool processRenderingEvent(RenderingEventCommand command) noexcept; - - /// Reads and processes a frames rendered event from `renderingEvents_` - bool processFramesRenderedEvent() noexcept; - - /// Called when the first audio frame from a decoder will render. - void handleRenderingWillStartEvent(Decoder _Nonnull decoder, uint64_t hostTime) noexcept; - - /// Called when the final audio frame from a decoder will render. - void handleRenderingWillCompleteEvent(Decoder _Nonnull decoder, uint64_t hostTime) noexcept; - - // MARK: - Active Decoder Management - - /// Cancels all active decoders in sequence - void cancelActiveDecoders() noexcept; - - /// Returns the first decoder state in `activeDecoders_` that has not been canceled - DecoderState *const _Nullable firstActiveDecoderState() const noexcept; - - public: - // MARK: - AVAudioEngine Notification Handling - - /// Called to process `AVAudioEngineConfigurationChangeNotification` - void handleAudioEngineConfigurationChange(AVAudioEngine *_Nonnull engine, - NSDictionary *_Nullable userInfo) noexcept; - -#if TARGET_OS_IPHONE - /// Called to process `AVAudioSessionInterruptionNotification` - void handleAudioSessionInterruption(NSDictionary *_Nullable userInfo) noexcept; -#endif /* TARGET_OS_IPHONE */ - - private: - // MARK: - Processing Graph Management - - /// Stops the AVAudioEngine if it is running and returns true if it was stopped - bool stopEngineIfRunning() noexcept; - - /// Configures the player to render audio with `format` - /// - parameter format: The desired audio format - /// - parameter error: An optional pointer to an `NSError` object to receive error information - /// - returns: `true` if the player was successfully configured - bool configureProcessingGraphAndRingBufferForFormat(AVAudioFormat *_Nonnull format, NSError **error) noexcept; -}; - -// MARK: - Implementation - - -inline void AudioPlayer::clearDecoderQueue() noexcept { - std::lock_guard lock{queuedDecodersLock_}; - queuedDecoders_.clear(); -} - -inline bool AudioPlayer::decoderQueueIsEmpty() const noexcept { - std::lock_guard lock{queuedDecodersLock_}; - return queuedDecoders_.empty(); -} - -inline SFBAudioPlayerPlaybackState AudioPlayer::playbackState() const noexcept { - const auto flags = flags_.load(std::memory_order_acquire); - constexpr auto mask = - static_cast(Flags::engineIsRunning) | static_cast(Flags::isPlaying); - const auto state = flags & mask; - assert(state != static_cast(Flags::isPlaying)); - return static_cast(state); -} - -inline bool AudioPlayer::isPlaying() const noexcept { - const auto flags = flags_.load(std::memory_order_acquire); - constexpr auto mask = - static_cast(Flags::engineIsRunning) | static_cast(Flags::isPlaying); - return (flags & mask) == mask; -} - -inline bool AudioPlayer::isPaused() const noexcept { - const auto flags = flags_.load(std::memory_order_acquire); - constexpr auto mask = - static_cast(Flags::engineIsRunning) | static_cast(Flags::isPlaying); - return (flags & mask) == static_cast(Flags::engineIsRunning); -} - -inline bool AudioPlayer::isStopped() const noexcept { - const auto flags = flags_.load(std::memory_order_acquire); - return !(flags & static_cast(Flags::engineIsRunning)); -} - -inline bool AudioPlayer::isReady() const noexcept { - std::lock_guard lock{activeDecodersLock_}; - return firstActiveDecoderState() != nullptr; -} - -inline AudioPlayer::Decoder _Nullable AudioPlayer::nowPlaying() const noexcept { - std::lock_guard lock{nowPlayingLock_}; - return nowPlaying_; -} - -inline AVAudioSourceNode *_Nonnull AudioPlayer::sourceNode() const noexcept { - return sourceNode_; -} - -inline AVAudioMixerNode *_Nonnull AudioPlayer::mainMixerNode() const noexcept { - return engine_.mainMixerNode; -} - -inline AVAudioOutputNode *_Nonnull AudioPlayer::outputNode() const noexcept { - return engine_.outputNode; -} - -} /* namespace sfb */ - -#pragma clang diagnostic pop diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm.orig b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm.orig deleted file mode 100644 index 0aa59998a..000000000 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm.orig +++ /dev/null @@ -1,2360 +0,0 @@ -// -// Copyright (c) 2006-2026 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#import "AudioPlayer.h" - -#import "SFBAudioDecoder.h" -#import "SFBAudioPlayer+Internal.h" -#import "SFBCStringForOSType.h" -#import "host_time.hpp" - -#import -#import -#import - -#import - -#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 -constexpr AVAudioFrameCount ringBufferChunkSize = 2048; - -/// The default decoding event ring buffer capacity -constexpr std::size_t decodingEventRingBufferCapacity = 2048; -/// The default rendering event ring buffer capacity -constexpr std::size_t renderingEventRingBufferCapacity = 4096; - -/// Objective-C associated object key indicating if a decoder has been canceled -constexpr char decoderIsCanceledKey = '\0'; - -void audioEngineConfigurationChangeNotificationCallback(CFNotificationCenterRef center, void *observer, - CFNotificationName name, const void *object, - CFDictionaryRef userInfo) { - auto that = static_cast(observer); - that->handleAudioEngineConfigurationChange((__bridge AVAudioEngine *)object, (__bridge NSDictionary *)userInfo); -} - -#if TARGET_OS_IPHONE -void audioSessionInterruptionNotificationCallback(CFNotificationCenterRef center, void *observer, - CFNotificationName name, const void *object, - CFDictionaryRef userInfo) { - auto that = static_cast(observer); - that->handleAudioSessionInterruption((__bridge NSDictionary *)userInfo); -} -#endif /* TARGET_OS_IPHONE */ - -#if !TARGET_OS_IPHONE -/// Returns the name of `audioUnit.deviceID` -/// -/// This is the value of `kAudioObjectPropertyName` in the output scope on the main element -NSString *_Nullable audioDeviceName(AUAudioUnit *_Nonnull audioUnit) noexcept { -#if DEBUG - assert(audioUnit != nil); -#endif /* DEBUG */ - - AudioObjectPropertyAddress address = {.mSelector = kAudioObjectPropertyName, - .mScope = kAudioObjectPropertyScopeOutput, - .mElement = kAudioObjectPropertyElementMain}; - CFStringRef name = nullptr; - UInt32 dataSize = sizeof(name); - const auto result = AudioObjectGetPropertyData(audioUnit.deviceID, &address, 0, nullptr, &dataSize, &name); - if (result != noErr) { - os_log_error(sfb::AudioPlayer::log_, - "AudioObjectGetPropertyData (kAudioObjectPropertyName, kAudioObjectPropertyScopeOutput, " - "kAudioObjectPropertyElementMain) failed: %d '%{public}.4s'", - result, SFBCStringForOSType(result)); - return nil; - } - return (__bridge_transfer NSString *)name; -} -#endif /* !TARGET_OS_IPHONE */ - -/// Returns a string describing `format` -NSString *stringDescribingAVAudioFormat(AVAudioFormat *_Nullable format, bool includeChannelLayout = true) noexcept { - if (!format) - return nil; - - NSString *formatDescription = CXXCoreAudio::AudioStreamBasicDescriptionFormatDescription(*format.streamDescription); - if (includeChannelLayout) { - NSString *layoutDescription = CXXCoreAudio::AudioChannelLayoutDescription(format.channelLayout.layout); - return [NSString stringWithFormat:@"", format, formatDescription, - layoutDescription ?: @"no channel layout"]; - } else { - return [NSString stringWithFormat:@"", format, formatDescription]; - } -} - -/// Returns the next event identification number -/// - note: Event identification numbers are unique across all event types -uint64_t nextEventIdentificationNumber() noexcept { - static std::atomic_uint64_t nextIdentificationNumber = 1; - static_assert(std::atomic_uint64_t::is_always_lock_free, "Lock-free std::atomic_uint64_t required"); - return nextIdentificationNumber.fetch_add(1, std::memory_order_relaxed); -} - -/// Performs a generic atomic read-modify-write (RMW) operation -/// - returns: The value before the operation -template - requires std::atomic::is_always_lock_free && std::is_trivially_copyable_v && std::invocable && - std::convertible_to, T> -T fetchUpdate(std::atomic& atom, Func&& func, - std::memory_order order = std::memory_order_seq_cst) noexcept(std::is_nothrow_invocable_v && - std::is_nothrow_copy_constructible_v) { - T expected = atom.load(std::memory_order_relaxed); - while (true) { - const T desired = func(expected); - if (atom.compare_exchange_weak(expected, desired, order, std::memory_order_relaxed)) - return expected; - } -} - -/// Returns the absolute difference between a and b -template - requires std::unsigned_integral -constexpr T absoluteDifference(T a, T b) noexcept { - return (a >= b) ? (a - b) : (b - a); -} - -} /* namespace */ - -namespace sfb { - -const os_log_t AudioPlayer::log_ = os_log_create("org.sbooth.AudioEngine", "AudioPlayer"); - -// MARK: - Decoder State - -/// State for tracking/syncing decoding progress -struct AudioPlayer::DecoderState final { - /// Next sequence number to use - static uint64_t sequenceCounter_; - - /// Monotonically increasing instance counter - const uint64_t sequenceNumber_{sequenceCounter_++}; - - /// Decodes audio from the source representation to PCM - const Decoder decoder_{nil}; - - /// The sample rate of the audio converter's output format - const double sampleRate_{0}; - - /// Flags - std::atomic_uint flags_{0}; - static_assert(std::atomic_uint::is_always_lock_free, "Lock-free std::atomic_uint required"); - - /// The number of frames decoded - std::atomic_int64_t framesDecoded_{0}; - /// The number of frames converted - std::atomic_int64_t framesConverted_{0}; - /// The number of frames rendered - std::atomic_int64_t framesRendered_{0}; - /// The total number of audio frames - std::atomic_int64_t frameLength_{0}; - /// The desired seek offset - std::atomic_int64_t seekOffset_{SFBUnknownFramePosition}; - - static_assert(std::atomic_int64_t::is_always_lock_free, "Lock-free std::atomic_int64_t required"); - - /// Converts audio from the decoder's processing format to the equivalent standard format - AVAudioConverter *converter_{nil}; - /// Buffer used internally for buffering during conversion - AVAudioPCMBuffer *decodeBuffer_{nil}; - - /// The error that caused decoding to abort, if any - NSError *error_{nil}; - - /// Possible bits in `flags_` - enum class Flags : unsigned int { - /// Decoding started - decodingStarted = 1u << 0, - /// Decoding complete - decodingComplete = 1u << 1, - /// Decoding was resumed after completion - decodingResumed = 1u << 2, - /// Decoding was suspended after starting - decodingSuspended = 1u << 3, - /// Rendering started - renderingStarted = 1u << 4, - /// A seek has been requested - seekPending = 1u << 5, - /// Decoder cancelation requested - cancelRequested = 1u << 6, - /// Decoder canceled - isCanceled = 1u << 7, - }; - - DecoderState(Decoder _Nonnull decoder) noexcept; - - bool allocate(AVAudioFrameCount frameCapacity) noexcept; - - AVAudioFramePosition framePosition() const noexcept; - AVAudioFramePosition frameLength() const noexcept; - - bool decodeAudio(AVAudioPCMBuffer *_Nonnull buffer, NSError **error) noexcept; - - /// Sets the pending seek request to `frame` - void requestSeekToFrame(AVAudioFramePosition frame) noexcept; - /// Performs the pending seek request - bool performSeek(NSError **error) noexcept; -}; - -uint64_t AudioPlayer::DecoderState::sequenceCounter_ = 1; - -inline AudioPlayer::DecoderState::DecoderState(Decoder _Nonnull decoder) noexcept - : decoder_{decoder}, frameLength_{decoder.frameLength}, sampleRate_{decoder.processingFormat.sampleRate} { -#if DEBUG - assert(decoder != nil); -#endif /* DEBUG */ -} - -inline bool AudioPlayer::DecoderState::allocate(AVAudioFrameCount frameCapacity) noexcept { - auto format = decoder_.processingFormat; - auto standardEquivalentFormat = format.standardEquivalent; - if (!standardEquivalentFormat) { - os_log_error(log_, "Error converting %{public}@ to standard equivalent format", - stringDescribingAVAudioFormat(format)); - return false; - } - - // Convert to deinterleaved native-endian float, preserving the channel count and order - converter_ = [[AVAudioConverter alloc] initFromFormat:format toFormat:standardEquivalentFormat]; - if (!converter_) { - os_log_error(log_, "Error creating AVAudioConverter converting from %{public}@ to %{public}@", - stringDescribingAVAudioFormat(format), stringDescribingAVAudioFormat(standardEquivalentFormat)); - return false; - } - - // The logic in this class assumes no SRC is performed by converter_ - assert(converter_.inputFormat.sampleRate == converter_.outputFormat.sampleRate); - - decodeBuffer_ = [[AVAudioPCMBuffer alloc] initWithPCMFormat:converter_.inputFormat frameCapacity:frameCapacity]; - if (!decodeBuffer_) - return false; - - if (const auto framePosition = decoder_.framePosition; framePosition != 0) { - framesDecoded_.store(framePosition, std::memory_order_release); - framesConverted_.store(framePosition, std::memory_order_release); - framesRendered_.store(framePosition, std::memory_order_release); - } - - return true; -} - -inline AVAudioFramePosition AudioPlayer::DecoderState::framePosition() const noexcept { - const bool seekPending = flags_.load(std::memory_order_acquire) & static_cast(Flags::seekPending); - return seekPending ? seekOffset_.load(std::memory_order_acquire) : framesRendered_.load(std::memory_order_acquire); -} - -inline AVAudioFramePosition AudioPlayer::DecoderState::frameLength() const noexcept { - return frameLength_.load(std::memory_order_acquire); -} - -inline bool AudioPlayer::DecoderState::decodeAudio(AVAudioPCMBuffer *_Nonnull buffer, NSError **error) noexcept { -#if DEBUG - assert(buffer != nil); - assert(buffer.frameCapacity == decodeBuffer_.frameCapacity); -#endif /* DEBUG */ - - if (![decoder_ decodeIntoBuffer:decodeBuffer_ frameLength:decodeBuffer_.frameCapacity error:error]) - return false; - - if (decodeBuffer_.frameLength == 0) { - flags_.fetch_or(static_cast(Flags::decodingComplete), std::memory_order_acq_rel); - -#if false - // Some formats may not know the exact number of frames in advance - // without processing the entire file, which is a potentially slow operation - frameLength_.store(mDecoder.framePosition, std::memory_order_release); -#endif /* false */ - - buffer.frameLength = 0; - return true; - } - - this->framesDecoded_.fetch_add(decodeBuffer_.frameLength, std::memory_order_acq_rel); - - // Only PCM to PCM conversions are performed - if (![converter_ convertToBuffer:buffer fromBuffer:decodeBuffer_ error:error]) - return false; - framesConverted_.fetch_add(buffer.frameLength, std::memory_order_acq_rel); - - // If `buffer` is not full but -decodeIntoBuffer:frameLength:error: returned `YES` - // decoding is complete - if (buffer.frameLength != buffer.frameCapacity) - flags_.fetch_or(static_cast(Flags::decodingComplete), std::memory_order_acq_rel); - - return true; -} - -/// Sets the pending seek request to `frame` -inline void AudioPlayer::DecoderState::requestSeekToFrame(AVAudioFramePosition frame) noexcept { - seekOffset_.store(frame, std::memory_order_release); - flags_.fetch_or(static_cast(Flags::seekPending), std::memory_order_acq_rel); -} - -/// Performs the pending seek request -inline bool AudioPlayer::DecoderState::performSeek(NSError **error) noexcept { -#if DEBUG - assert(flags_.load(std::memory_order_acquire) & static_cast(Flags::seekPending)); -#endif /* DEBUG */ - - auto seekOffset = seekOffset_.load(std::memory_order_acquire); - os_log_debug(log_, "Seeking to frame %lld in %{public}@ ", seekOffset, decoder_); - - if (NSError *seekError = nil; ![decoder_ seekToFrame:seekOffset error:&seekError]) { - os_log_error(log_, "Error seeking to frame %lld in %{public}@", seekOffset, decoder_); - if (error) - *error = seekError; - return false; - } - - // Reset the converter to flush any buffers - [converter_ reset]; - - const auto newFrame = decoder_.framePosition; - if (newFrame != seekOffset) { - os_log_info(log_, "Inaccurate seek to frame %lld, got %lld", seekOffset, newFrame); - seekOffset = newFrame; - } - - // Clear the seek request - flags_.fetch_and(~static_cast(Flags::seekPending), std::memory_order_acq_rel); - - // Update the frame counters accordingly - // A seek is handled in essentially the same way as initial playback - if (newFrame != SFBUnknownFramePosition) { - framesDecoded_.store(newFrame, std::memory_order_release); - framesConverted_.store(seekOffset, std::memory_order_release); - framesRendered_.store(seekOffset, std::memory_order_release); - } - - return newFrame != SFBUnknownFramePosition; -} - -} /* namespace sfb */ - -// MARK: - AudioPlayer - -sfb::AudioPlayer::AudioPlayer() { - // ======================================== - // Rendering Setup - - // Start out with 44.1 kHz stereo - AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100 channels:2]; - if (!format) { - os_log_error(log_, "Unable to create AVAudioFormat for 44.1 kHz stereo"); - 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)) { - os_log_error(log_, - "Unable to create audio ring buffer: CXXCoreAudio::AudioRingBuffer::Allocate failed with format " - "%{public}@ and capacity %zu", - CXXCoreAudio::AudioStreamBasicDescriptionFormatDescription(*(format.streamDescription)), - ringBufferCapacity); - throw std::runtime_error("CXXCoreAudio::AudioRingBuffer::Allocate failed"); - } - - // ======================================== - // Event Processing Setup - - // The decoding event ring buffer is written to by the decoding thread and read from by the event queue - if (!decodingEvents_.Allocate(decodingEventRingBufferCapacity)) { - os_log_error(log_, - "Unable to create decoding event ring buffer: sfb::RingBuffer::Allocate failed with capacity %zu", - decodingEventRingBufferCapacity); - throw std::runtime_error("CXXRingBuffer::RingBuffer::Allocate failed"); - } - - decodingSemaphore_ = dispatch_semaphore_create(0); - if (!decodingSemaphore_) { - os_log_error(log_, "Unable to create decoding event semaphore: dispatch_semaphore_create failed"); - throw std::runtime_error("Unable to create decoding event dispatch semaphore"); - } - - // The rendering event ring buffer is written to by the render block and read from by the event queue - if (!renderingEvents_.Allocate(renderingEventRingBufferCapacity)) { - os_log_error(log_, - "Unable to create rendering event ring buffer: sfb::RingBuffer::Allocate failed with capacity %zu", - renderingEventRingBufferCapacity); - throw std::runtime_error("CXXRingBuffer::RingBuffer::Allocate failed"); - } - - eventSemaphore_ = dispatch_semaphore_create(0); - if (!eventSemaphore_) { - os_log_error(log_, "Unable to create rendering event semaphore: dispatch_semaphore_create failed"); - throw std::runtime_error("Unable to create rendering event dispatch semaphore"); - } - - // Create the dispatch queue used for event processing - auto attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); - if (!attr) { - os_log_error(log_, "dispatch_queue_attr_make_with_qos_class failed"); - throw std::runtime_error("dispatch_queue_attr_make_with_qos_class failed"); - } - - eventQueue_ = dispatch_queue_create_with_target("AudioPlayer.Events", attr, DISPATCH_TARGET_QUEUE_DEFAULT); - if (!eventQueue_) { - os_log_error(log_, "Unable to create event dispatch queue: dispatch_queue_create failed"); - throw std::runtime_error("dispatch_queue_create_with_target failed"); - } - -<<<<<<< HEAD - // Launch the decoding and event processing threads - try { -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - decodingThread_ = std::jthread(std::bind_front(&sfb::AudioPlayer::processDecoders, this)); - eventThread_ = std::jthread(std::bind_front(&sfb::AudioPlayer::sequenceAndProcessEvents, this)); -#else - decodingThread_ = std::thread(&sfb::AudioPlayer::processDecoders, this); - eventThread_ = std::thread(&sfb::AudioPlayer::sequenceAndProcessEvents, this); -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - } catch(const std::exception& e) { - os_log_error(log_, "Unable to create thread: %{public}s", e.what()); - throw; - } -======= - // Launch the decoding and event processing threads - try { - decodingThread_ = std::jthread(std::bind_front(&sfb::AudioPlayer::processDecoders, this)); - eventThread_ = std::jthread(std::bind_front(&sfb::AudioPlayer::sequenceAndProcessEvents, this)); - } catch (const std::exception& e) { - os_log_error(log_, "Unable to create thread: %{public}s", e.what()); - throw; - } ->>>>>>> main - - // ======================================== - // Audio Processing Graph Setup - - engine_ = [[AVAudioEngine alloc] init]; - if (!engine_) { - os_log_error(log_, "Unable to create AVAudioEngine instance"); - throw std::runtime_error("Unable to create AVAudioEngine"); - } - - sourceNode_ = [[AVAudioSourceNode alloc] - initWithRenderBlock:^OSStatus(BOOL *isSilence, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, - AudioBufferList *outputData) { - return render(*isSilence, *timestamp, frameCount, outputData); - }]; - if (!sourceNode_) - throw std::runtime_error("Unable to create AVAudioSourceNode instance"); - - [engine_ attachNode:sourceNode_]; - [engine_ connect:sourceNode_ to:engine_.mainMixerNode format:format]; - [engine_ prepare]; - -#if DEBUG - logProcessingGraphDescription(log_, OS_LOG_TYPE_DEBUG); -#endif /* DEBUG */ - - // Register for configuration change notifications - auto notificationCenter = CFNotificationCenterGetLocalCenter(); - CFNotificationCenterAddObserver(notificationCenter, this, audioEngineConfigurationChangeNotificationCallback, - (__bridge CFStringRef)AVAudioEngineConfigurationChangeNotification, - (__bridge void *)engine_, CFNotificationSuspensionBehaviorDeliverImmediately); - -#if TARGET_OS_IPHONE - // Register for audio session interruption notifications - CFNotificationCenterAddObserver(notificationCenter, this, audioSessionInterruptionNotificationCallback, - (__bridge CFStringRef)AVAudioSessionInterruptionNotification, - (__bridge void *)[AVAudioSession sharedInstance], - CFNotificationSuspensionBehaviorDeliverImmediately); -#endif /* TARGET_OS_IPHONE */ -} - -sfb::AudioPlayer::~AudioPlayer() noexcept { - auto notificationCenter = CFNotificationCenterGetLocalCenter(); - CFNotificationCenterRemoveEveryObserver(notificationCenter, this); - - { - std::lock_guard lock{engineLock_}; - [engine_ stop]; - flags_.fetch_and(~static_cast(Flags::engineIsRunning) & - ~static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - } - - clearDecoderQueue(); - cancelActiveDecoders(); - -<<<<<<< HEAD -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - // Register a stop callback for the decoding thread - std::stop_callback decodingThreadStopCallback(decodingThread_.get_stop_token(), [this] { - dispatch_semaphore_signal(decodingSemaphore_); - }); - - // Issue a stop request to the decoding thread and wait for it to exit - decodingThread_.request_stop(); -#else - // Stop the decoding thread - flags_.fetch_or(static_cast(Flags::stopDecodingThread), std::memory_order_acq_rel); - dispatch_semaphore_signal(decodingSemaphore_); -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - - try { - decodingThread_.join(); - } catch(const std::exception& e) { - os_log_error(log_, "Unable to join decoding thread: %{public}s", e.what()); - } - - -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - // Register a stop callback for the event processing thread - std::stop_callback eventThreadStopCallback(eventThread_.get_stop_token(), [this] { - dispatch_semaphore_signal(eventSemaphore_); - }); - - // Issue a stop request to the event processing thread and wait for it to exit - eventThread_.request_stop(); -#else - // Stop the event processing thread - flags_.fetch_or(static_cast(Flags::stopEventThread), std::memory_order_acq_rel); - dispatch_semaphore_signal(eventSemaphore_); -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - - try { - eventThread_.join(); - } catch(const std::exception& e) { - os_log_error(log_, "Unable to join event processing thread: %{public}s", e.what()); - } -======= - // Register a stop callback for the decoding thread - std::stop_callback decodingThreadStopCallback(decodingThread_.get_stop_token(), - [this] { dispatch_semaphore_signal(decodingSemaphore_); }); - - // Issue a stop request to the decoding thread and wait for it to exit - decodingThread_.request_stop(); - try { - decodingThread_.join(); - } catch (const std::exception& e) { - os_log_error(log_, "Unable to join decoding thread: %{public}s", e.what()); - } - - // Register a stop callback for the event processing thread - std::stop_callback eventThreadStopCallback(eventThread_.get_stop_token(), - [this] { dispatch_semaphore_signal(eventSemaphore_); }); - - // Issue a stop request to the event processing thread and wait for it to exit - eventThread_.request_stop(); - try { - eventThread_.join(); - } catch (const std::exception& e) { - os_log_error(log_, "Unable to join event processing thread: %{public}s", e.what()); - } ->>>>>>> main - - // Delete any remaining decoder state - activeDecoders_.clear(); - - os_log_debug(log_, " destroyed", this); -} - -// MARK: - Playlist Management - -bool sfb::AudioPlayer::enqueueDecoder(Decoder decoder, bool forImmediatePlayback, NSError **error) noexcept { -#if DEBUG - assert(decoder != nil); -#endif /* DEBUG */ - - // Open the decoder if necessary - if (!decoder.isOpen && ![decoder openReturningError:error]) - return false; - - // Ensure only one decoder can be enqueued at a time - std::lock_guard lock{queuedDecodersLock_}; - - if (forImmediatePlayback) - queuedDecoders_.clear(); - - try { - queuedDecoders_.push_back(decoder); - } catch (const std::exception& e) { - os_log_error(log_, "Error pushing %{public}@ to queuedDecoders_: %{public}s", decoder, e.what()); - if (error) - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]; - return false; - } - - os_log_info(log_, "Enqueued %{public}@", decoder); - - if (forImmediatePlayback) { - cancelActiveDecoders(); - // Mute until the decoder becomes active - flags_.fetch_or(static_cast(Flags::isMuted), std::memory_order_acq_rel); - } - - dispatch_semaphore_signal(decodingSemaphore_); - - return true; -} - -bool sfb::AudioPlayer::formatWillBeGaplessIfEnqueued(AVAudioFormat *format) const noexcept { -#if DEBUG - assert(format != nil); -#endif /* DEBUG */ - // Gapless playback requires the same number of channels at the same sample rate with the same channel layout - auto renderFormat = [sourceNode_ outputFormatForBus:0]; - return format.channelCount == renderFormat.channelCount && format.sampleRate == renderFormat.sampleRate && - CXXCoreAudio::AVAudioChannelLayoutsAreEquivalent(format.channelLayout, renderFormat.channelLayout); -} - -// MARK: - Playback Control - -bool sfb::AudioPlayer::play(NSError **error) noexcept { - auto didStartEngine = false; - auto wasPlaying = false; - { - std::lock_guard lock{engineLock_}; - if (didStartEngine = !engine_.isRunning; didStartEngine) { - if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { - os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); - flags_.fetch_and(~static_cast(Flags::engineIsRunning) & - ~static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - if (error) - *error = startError; - return false; - } - } - - const auto prevFlags = flags_.fetch_or(static_cast(Flags::engineIsRunning) | - static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - wasPlaying = prevFlags & static_cast(Flags::isPlaying); - assert(!(didStartEngine && wasPlaying)); - } - - if ((didStartEngine || !wasPlaying) && [player_.delegate respondsToSelector:@selector(audioPlayer: - playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ playbackStateChanged:SFBAudioPlayerPlaybackStatePlaying]; - - return true; -} - -bool sfb::AudioPlayer::pause() noexcept { - auto wasPlaying = false; - { - std::lock_guard lock{engineLock_}; - if (!engine_.isRunning) - return false; - const auto prevFlags = - flags_.fetch_and(~static_cast(Flags::isPlaying), std::memory_order_acq_rel); - wasPlaying = prevFlags & static_cast(Flags::isPlaying); - } - - if (wasPlaying && [player_.delegate respondsToSelector:@selector(audioPlayer:playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ playbackStateChanged:SFBAudioPlayerPlaybackStatePaused]; - - return true; -} - -bool sfb::AudioPlayer::resume() noexcept { - auto wasPaused = false; - { - std::lock_guard lock{engineLock_}; - if (!engine_.isRunning) - return false; - const auto prevFlags = flags_.fetch_or(static_cast(Flags::isPlaying), std::memory_order_acq_rel); - wasPaused = !(prevFlags & static_cast(Flags::isPlaying)); - } - - if (wasPaused && [player_.delegate respondsToSelector:@selector(audioPlayer:playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ playbackStateChanged:SFBAudioPlayerPlaybackStatePlaying]; - - return true; -} - -void sfb::AudioPlayer::stop() noexcept { - const auto didStopEngine = stopEngineIfRunning(); - - clearDecoderQueue(); - cancelActiveDecoders(); - - if (didStopEngine && [player_.delegate respondsToSelector:@selector(audioPlayer:playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ playbackStateChanged:SFBAudioPlayerPlaybackStateStopped]; -} - -bool sfb::AudioPlayer::togglePlayPause(NSError **error) noexcept { - SFBAudioPlayerPlaybackState playbackState; - { - std::lock_guard lock{engineLock_}; - - // Currently stopped, transition to playing - if (!engine_.isRunning) { - if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { - os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); - flags_.fetch_and(~static_cast(Flags::engineIsRunning) & - ~static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - if (error) - *error = startError; - return false; - } - - const auto prevFlags = flags_.fetch_or(static_cast(Flags::engineIsRunning) | - static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - assert(!(prevFlags & static_cast(Flags::isPlaying))); - - playbackState = SFBAudioPlayerPlaybackStatePlaying; - } else { - // Toggle playing/paused - if (flags_.fetch_xor(static_cast(Flags::isPlaying), std::memory_order_acq_rel) & - static_cast(Flags::isPlaying)) - playbackState = SFBAudioPlayerPlaybackStatePaused; - else - playbackState = SFBAudioPlayerPlaybackStatePlaying; - } - } - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ playbackStateChanged:playbackState]; - - return true; -} - -void sfb::AudioPlayer::reset() noexcept { - { - std::lock_guard lock{engineLock_}; - [engine_ reset]; - } - clearDecoderQueue(); - cancelActiveDecoders(); -} - -// MARK: - Player State - -bool sfb::AudioPlayer::engineIsRunning() const noexcept { - const auto isRunning = engine_.isRunning; -#if DEBUG - assert(static_cast(flags_.load(std::memory_order_acquire) & - static_cast(Flags::engineIsRunning)) == isRunning && - "Cached value for engine_.isRunning invalid"); -#endif /* DEBUG */ - return isRunning; -} - -sfb::AudioPlayer::Decoder sfb::AudioPlayer::currentDecoder() const noexcept { - std::lock_guard lock{activeDecodersLock_}; - const auto *decoderState = firstActiveDecoderState(); - if (!decoderState) - return nil; - return decoderState->decoder_; -} - -void sfb::AudioPlayer::setNowPlaying(Decoder nowPlaying) noexcept { - Decoder previouslyPlaying = nil; - { - std::lock_guard lock{nowPlayingLock_}; - if (nowPlaying_ == nowPlaying) - return; - previouslyPlaying = nowPlaying_; - nowPlaying_ = nowPlaying; - } - - os_log_debug(log_, "Now playing changed to %{public}@", nowPlaying); - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:nowPlayingChanged:previouslyPlaying:)]) - [player_.delegate audioPlayer:player_ nowPlayingChanged:nowPlaying previouslyPlaying:previouslyPlaying]; -} - -// MARK: - Playback Properties - -SFBPlaybackPosition sfb::AudioPlayer::playbackPosition() const noexcept { - std::lock_guard lock{activeDecodersLock_}; - const auto *decoderState = firstActiveDecoderState(); - if (!decoderState) - return SFBInvalidPlaybackPosition; - return {.framePosition = decoderState->framePosition(), .frameLength = decoderState->frameLength()}; -} - -SFBPlaybackTime sfb::AudioPlayer::playbackTime() const noexcept { - std::lock_guard lock{activeDecodersLock_}; - - const auto *decoderState = firstActiveDecoderState(); - if (!decoderState) - return SFBInvalidPlaybackTime; - - SFBPlaybackTime playbackTime = SFBInvalidPlaybackTime; - - const auto framePosition = decoderState->framePosition(); - const auto frameLength = decoderState->frameLength(); - - if (const auto sampleRate = decoderState->sampleRate_; sampleRate > 0) { - if (framePosition != SFBUnknownFramePosition) - playbackTime.currentTime = framePosition / sampleRate; - if (frameLength != SFBUnknownFrameLength) - playbackTime.totalTime = frameLength / sampleRate; - } - - return playbackTime; -} - -bool sfb::AudioPlayer::getPlaybackPositionAndTime(SFBPlaybackPosition *playbackPosition, - SFBPlaybackTime *playbackTime) const noexcept { - std::lock_guard lock{activeDecodersLock_}; - - const auto *decoderState = firstActiveDecoderState(); - if (!decoderState) { - if (playbackPosition) - *playbackPosition = SFBInvalidPlaybackPosition; - if (playbackTime) - *playbackTime = SFBInvalidPlaybackTime; - return false; - } - - SFBPlaybackPosition currentPlaybackPosition = {.framePosition = decoderState->framePosition(), - .frameLength = decoderState->frameLength()}; - if (playbackPosition) - *playbackPosition = currentPlaybackPosition; - - if (playbackTime) { - SFBPlaybackTime currentPlaybackTime = SFBInvalidPlaybackTime; - if (const auto sampleRate = decoderState->sampleRate_; sampleRate > 0) { - if (currentPlaybackPosition.framePosition != SFBUnknownFramePosition) - currentPlaybackTime.currentTime = currentPlaybackPosition.framePosition / sampleRate; - if (currentPlaybackPosition.frameLength != SFBUnknownFrameLength) - currentPlaybackTime.totalTime = currentPlaybackPosition.frameLength / sampleRate; - } - *playbackTime = currentPlaybackTime; - } - - return true; -} - -// MARK: - Seeking - -bool sfb::AudioPlayer::seekInTime(NSTimeInterval secondsToSkip) noexcept { - std::lock_guard lock{activeDecodersLock_}; - - auto *decoderState = firstActiveDecoderState(); - if (!decoderState || !decoderState->decoder_.supportsSeeking) - return false; - - if (secondsToSkip == 0) - return true; - - const auto sampleRate = decoderState->sampleRate_; - const auto framePosition = decoderState->framePosition(); - const auto frameLength = decoderState->frameLength(); - - auto targetFrame = framePosition + static_cast(secondsToSkip * sampleRate); - targetFrame = std::clamp(targetFrame, 0LL, frameLength - 1); - - decoderState->requestSeekToFrame(targetFrame); - dispatch_semaphore_signal(decodingSemaphore_); - - return true; -} - -bool sfb::AudioPlayer::seekToTime(NSTimeInterval timeInSeconds) noexcept { - std::lock_guard lock{activeDecodersLock_}; - - auto *decoderState = firstActiveDecoderState(); - if (!decoderState || !decoderState->decoder_.supportsSeeking) - return false; - - const auto sampleRate = decoderState->sampleRate_; - const auto frameLength = decoderState->frameLength(); - - auto targetFrame = static_cast(timeInSeconds * sampleRate); - targetFrame = std::clamp(targetFrame, 0LL, frameLength - 1); - - decoderState->requestSeekToFrame(targetFrame); - dispatch_semaphore_signal(decodingSemaphore_); - - return true; -} - -bool sfb::AudioPlayer::seekToPosition(double position) noexcept { - position = std::clamp(position, 0.0, std::nextafter(1.0, 0.0)); - - std::lock_guard lock{activeDecodersLock_}; - - auto *decoderState = firstActiveDecoderState(); - if (!decoderState || !decoderState->decoder_.supportsSeeking) - return false; - - const auto frameLength = decoderState->frameLength(); - const auto targetFrame = static_cast(frameLength * position); - - decoderState->requestSeekToFrame(targetFrame); - dispatch_semaphore_signal(decodingSemaphore_); - - return true; -} - -bool sfb::AudioPlayer::seekToFrame(AVAudioFramePosition frame) noexcept { - std::lock_guard lock{activeDecodersLock_}; - - auto *decoderState = firstActiveDecoderState(); - if (!decoderState || !decoderState->decoder_.supportsSeeking) - return false; - - const auto frameLength = decoderState->frameLength(); - frame = std::clamp(frame, 0LL, frameLength - 1); - - decoderState->requestSeekToFrame(frame); - dispatch_semaphore_signal(decodingSemaphore_); - - return true; -} - -bool sfb::AudioPlayer::supportsSeeking() const noexcept { - std::lock_guard lock{activeDecodersLock_}; - const auto *decoderState = firstActiveDecoderState(); - if (!decoderState) - return false; - return decoderState->decoder_.supportsSeeking; -} - -#if !TARGET_OS_IPHONE - -// MARK: - Volume Control - -float sfb::AudioPlayer::volumeForChannel(AudioObjectPropertyElement channel) const noexcept { - AudioUnitParameterValue volume; - const auto result = AudioUnitGetParameter(engine_.outputNode.audioUnit, kHALOutputParam_Volume, - kAudioUnitScope_Global, channel, &volume); - if (result != noErr) { - os_log_error( - log_, - "AudioUnitGetParameter (kHALOutputParam_Volume, kAudioUnitScope_Global, %u) failed: %d '%{public}.4s'", - channel, result, SFBCStringForOSType(result)); - return std::nanf("1"); - } - - return volume; -} - -bool sfb::AudioPlayer::setVolumeForChannel(float volume, AudioObjectPropertyElement channel, NSError **error) noexcept { - os_log_info(log_, "Setting volume for channel %u to %g", channel, volume); - - const auto result = AudioUnitSetParameter(engine_.outputNode.audioUnit, kHALOutputParam_Volume, - kAudioUnitScope_Global, channel, volume, 0); - if (result != noErr) { - os_log_error( - log_, - "AudioUnitSetParameter (kHALOutputParam_Volume, kAudioUnitScope_Global, %u) failed: %d '%{public}.4s'", - channel, result, SFBCStringForOSType(result)); - if (error) - *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; - return false; - } - - return true; -} - -// MARK: - Output Device - -AUAudioObjectID sfb::AudioPlayer::outputDeviceID() const noexcept { - return engine_.outputNode.AUAudioUnit.deviceID; -} - -bool sfb::AudioPlayer::setOutputDeviceID(AUAudioObjectID outputDeviceID, NSError **error) noexcept { - os_log_info(log_, "Setting output device to 0x%x", this, outputDeviceID); - - if (NSError *err = nil; ![engine_.outputNode.AUAudioUnit setDeviceID:outputDeviceID error:&err]) { - os_log_error(log_, "Error setting output device: %{public}@", err); - if (error) - *error = err; - return false; - } - - return true; -} - -#endif /* !TARGET_OS_IPHONE */ - -// MARK: - AVAudioEngine - -void sfb::AudioPlayer::modifyProcessingGraph(void (^block)(AVAudioEngine *engine)) const noexcept { -#if DEBUG - assert(block != nil); -#endif /* DEBUG */ - - std::lock_guard lock{engineLock_}; - block(engine_); - - assert([engine_ inputConnectionPointForNode:engine_.outputNode inputBus:0].node == engine_.mainMixerNode && - "Illegal AVAudioEngine configuration"); - assert(engine_.isRunning == static_cast(flags_.load(std::memory_order_acquire) & - static_cast(Flags::engineIsRunning)) && - "AVAudioEngine may not be started or stopped outside of AudioPlayer"); -} - -// MARK: - Debugging - -void sfb::AudioPlayer::logProcessingGraphDescription(os_log_t log, os_log_type_t type) const noexcept { - NSMutableString *string = [NSMutableString stringWithFormat:@" audio processing graph:\n", this]; - - const auto engine = engine_; - const auto sourceNode = sourceNode_; - - AVAudioFormat *inputFormat = nil; - AVAudioFormat *outputFormat = [sourceNode outputFormatForBus:0]; - [string appendFormat:@"→ %@\n %@\n", sourceNode, stringDescribingAVAudioFormat(outputFormat)]; - - AVAudioConnectionPoint *connectionPoint = [[engine outputConnectionPointsForNode:sourceNode - outputBus:0] firstObject]; - while (connectionPoint.node != engine.mainMixerNode) { - inputFormat = [connectionPoint.node inputFormatForBus:connectionPoint.bus]; - outputFormat = [connectionPoint.node outputFormatForBus:connectionPoint.bus]; - if (![outputFormat isEqual:inputFormat]) - [string appendFormat:@"→ %@\n %@\n", connectionPoint.node, stringDescribingAVAudioFormat(outputFormat)]; - else - [string appendFormat:@"→ %@\n", connectionPoint.node]; - - connectionPoint = [[engine outputConnectionPointsForNode:connectionPoint.node outputBus:0] firstObject]; - } - - inputFormat = [engine.mainMixerNode inputFormatForBus:0]; - outputFormat = [engine.mainMixerNode outputFormatForBus:0]; - if (![outputFormat isEqual:inputFormat]) - [string appendFormat:@"→ %@\n %@\n", engine.mainMixerNode, stringDescribingAVAudioFormat(outputFormat)]; - else - [string appendFormat:@"→ %@\n", engine.mainMixerNode]; - - inputFormat = [engine.outputNode inputFormatForBus:0]; - outputFormat = [engine.outputNode outputFormatForBus:0]; - if (![outputFormat isEqual:inputFormat]) - [string appendFormat:@"→ %@\n %@]", engine.outputNode, stringDescribingAVAudioFormat(outputFormat)]; - else - [string appendFormat:@"→ %@", engine.outputNode]; - -#if !TARGET_OS_IPHONE - [string appendFormat:@"\n↓ \"%@\"", audioDeviceName(engine.outputNode.AUAudioUnit)]; -#endif /* !TARGET_OS_IPHONE */ - - os_log_with_type(log, type, "%{public}@", string); -} - -// MARK: - Decoding - -<<<<<<< HEAD -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L -void sfb::AudioPlayer::processDecoders(std::stop_token stoken) noexcept -#else -void sfb::AudioPlayer::processDecoders() noexcept -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ -{ - pthread_setname_np("AudioPlayer.Decoding"); - pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); -======= -void sfb::AudioPlayer::processDecoders(std::stop_token stoken) noexcept { - pthread_setname_np("AudioPlayer.Decoding"); - pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); ->>>>>>> main - - os_log_debug(log_, " decoding thread starting", this); - - // The buffer between the decoder state and the ring buffer - AVAudioPCMBuffer *buffer = nil; - // Whether there is a mismatch between the rendering format and the next decoder's processing format - auto formatMismatch = false; - -<<<<<<< HEAD - // Returns true if the decoding thread should exit - const auto stop_requested = [&] { -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - return stoken.stop_requested(); -#else - return (flags_.load(std::memory_order_acquire) & static_cast(Flags::stopDecodingThread)); -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - }; - - while(!stop_requested()) { - // The decoder state being processed - DecoderState *decoderState = nullptr; - auto ringBufferStale = false; -======= - while (!stoken.stop_requested()) { - // The decoder state being processed - DecoderState *decoderState = nullptr; - auto ringBufferStale = false; ->>>>>>> main - - { - std::lock_guard lock{activeDecodersLock_}; - - // Process cancellations - auto signal = false; - for (const auto& decoderState : activeDecoders_) { - if (const auto flags = decoderState->flags_.load(std::memory_order_acquire); - !(flags & static_cast(DecoderState::Flags::cancelRequested))) - continue; - - if (!decoderState->error_) - os_log_debug(log_, "Canceling decoding for %{public}@", decoderState->decoder_); - else - os_log_error(log_, "Aborting decoding for %{public}@ due to error", decoderState->decoder_); - - decoderState->flags_.fetch_or(static_cast(DecoderState::Flags::isCanceled), - std::memory_order_acq_rel); - ringBufferStale = true; - - // Submit the decoder canceled event - if (decodingEvents_.WriteValues(DecodingEventCommand::canceled, nextEventIdentificationNumber(), - decoderState->sequenceNumber_)) - signal = true; - else - os_log_fault(log_, "Error writing decoder canceled event"); - } - - // Signal the event thread if any decoders were canceled - if (signal) - dispatch_semaphore_signal(eventSemaphore_); - - // Get the earliest decoder state that has not completed rendering - decoderState = firstActiveDecoderState(); - } - - // Process pending seeks - if (decoderState) { - if (const auto flags = decoderState->flags_.load(std::memory_order_acquire); - flags & static_cast(DecoderState::Flags::seekPending)) { - if (NSError *seekError = nil; !decoderState->performSeek(&seekError)) { - decoderState->error_ = seekError; - decoderState->flags_.fetch_or(static_cast(DecoderState::Flags::cancelRequested), - std::memory_order_acq_rel); - continue; - } - ringBufferStale = true; - - if (flags & static_cast(DecoderState::Flags::decodingComplete)) { - os_log_debug(log_, "Resuming decoding for %{public}@", decoderState->decoder_); - - // The decoder has not completed rendering so the ring buffer format and the decoder's format still - // match. Clear the format mismatch flag so rendering can continue; the flag will be set again when - // decoding completes. - formatMismatch = false; - - fetchUpdate( - decoderState->flags_, - [](auto val) noexcept { - return (val & ~static_cast(DecoderState::Flags::decodingComplete)) | - static_cast(DecoderState::Flags::decodingResumed); - }, - std::memory_order_acq_rel); - - { - std::lock_guard lock{activeDecodersLock_}; - - // Rewind ensuing decoder states if possible to avoid discarding frames - for (const auto& nextDecoderState : activeDecoders_) { - if (nextDecoderState->sequenceNumber_ <= decoderState->sequenceNumber_) - continue; - - if (const auto flags = nextDecoderState->flags_.load(std::memory_order_acquire); - flags & (static_cast(DecoderState::Flags::isCanceled))) - continue; - else if (flags & static_cast(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)) { - nextDecoderState->error_ = seekError; - nextDecoderState->flags_.fetch_or( - static_cast(DecoderState::Flags::cancelRequested), - std::memory_order_acq_rel); - continue; - } - } else { - os_log_error(log_, "Discarding %lld frames from %{public}@", - nextDecoderState->framesDecoded_.load(std::memory_order_acquire), - nextDecoderState->decoder_); - } - - fetchUpdate( - nextDecoderState->flags_, - [](auto val) noexcept { - return (val & - ~static_cast(DecoderState::Flags::decodingStarted)) | - static_cast(DecoderState::Flags::decodingSuspended); - }, - std::memory_order_acq_rel); - } - } - } - } - } - } - - // Request a drain of the ring buffer during the next render cycle to prevent audible artifacts from seeking or - // cancellation - if (ringBufferStale) - flags_.fetch_or(static_cast(Flags::drainRequired), std::memory_order_acq_rel); - - // Get the earliest decoder state that has not completed decoding - { - std::lock_guard lock{activeDecodersLock_}; - - const auto iter = std::ranges::find_if(activeDecoders_, [](const auto& decoderState) { - const auto flags = decoderState->flags_.load(std::memory_order_acquire); - constexpr auto mask = static_cast(DecoderState::Flags::isCanceled) | - static_cast(DecoderState::Flags::decodingComplete); - return !(flags & mask); - }); - - if (iter != activeDecoders_.cend()) - decoderState = (*iter).get(); - else - decoderState = nullptr; - } - - // Dequeue the next decoder if there are no decoders that haven't completed decoding - if (!decoderState) { - { - // Lock both mutexes to ensure a decoder doesn't momentarily "disappear" - // when transitioning from queued to active - std::scoped_lock lock{queuedDecodersLock_, activeDecodersLock_}; - - if (!queuedDecoders_.empty()) { - // Remove the first decoder from the decoder queue - auto decoder = queuedDecoders_.front(); - queuedDecoders_.pop_front(); - - // Create the decoder state and add it to the list of active decoders - try { - activeDecoders_.push_back(std::make_unique(decoder)); -#if DEBUG - assert(std::ranges::is_sorted(activeDecoders_, std::ranges::less{}, - &DecoderState::sequenceNumber_)); -#endif /* DEBUG */ - decoderState = activeDecoders_.back().get(); - } catch (const std::exception& e) { - os_log_error(log_, "Error creating decoder state for %{public}@: %{public}s", decoder, - e.what()); - submitDecodingErrorEvent([NSError errorWithDomain:SFBAudioPlayerErrorDomain - code:SFBAudioPlayerErrorCodeInternalError - userInfo:nil]); - continue; - } - } - } - - if (decoderState) { - // Allocate decoder state internals - if (!decoderState->allocate(ringBufferChunkSize)) { - os_log_error(log_, - "Error allocating decoder state data: DecoderStateData::allocate failed with frame " - "capacity %d", - ringBufferChunkSize); - decoderState->error_ = [NSError errorWithDomain:SFBAudioPlayerErrorDomain - code:SFBAudioPlayerErrorCodeInternalError - userInfo:nil]; - decoderState->flags_.fetch_or(static_cast(DecoderState::Flags::cancelRequested), - std::memory_order_acq_rel); - continue; - } - - os_log_debug(log_, "Dequeued %{public}@", decoderState->decoder_); - } - } - - if (decoderState) { - // Before decoding starts determine the decoder and ring buffer format compatibility - if (!(decoderState->flags_.load(std::memory_order_acquire) & - static_cast(DecoderState::Flags::decodingStarted))) { - // Start decoding immediately if the join will be gapless (same sample rate, channel count, and channel - // layout) - if (auto renderFormat = decoderState->converter_.outputFormat; - [renderFormat isEqual:[sourceNode_ outputFormatForBus:0]]) { - // 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) { - buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:renderFormat - frameCapacity:ringBufferChunkSize]; - if (!buffer) { - os_log_error(log_, - "Error creating AVAudioPCMBuffer with format %{public}@ and frame capacity %d", - stringDescribingAVAudioFormat(renderFormat), ringBufferChunkSize); - decoderState->error_ = [NSError errorWithDomain:SFBAudioPlayerErrorDomain - code:SFBAudioPlayerErrorCodeInternalError - userInfo:nil]; - decoderState->flags_.fetch_or( - static_cast(DecoderState::Flags::cancelRequested), - std::memory_order_acq_rel); - continue; - } - } - } else { - // If the next decoder cannot be gaplessly joined set the mismatch flag and wait; - // decoding can't start until the processing graph is reconfigured which occurs after - // all active decoders complete - formatMismatch = true; - } - } - - // If there is a format mismatch the processing graph requires reconfiguration before decoding can begin - if (formatMismatch) { - // Wait until all other decoders complete processing before reconfiguring the graph - const auto okToReconfigure = [&] { - std::lock_guard lock{activeDecodersLock_}; - return activeDecoders_.size() == 1; - }(); - - if (okToReconfigure) { - flags_.fetch_and(~static_cast(Flags::drainRequired), std::memory_order_release); - 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)) { - decoderState->error_ = error; - decoderState->flags_.fetch_or(static_cast(DecoderState::Flags::cancelRequested), - std::memory_order_acq_rel); - continue; - } - - // 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) { - buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:renderFormat - frameCapacity:ringBufferChunkSize]; - if (!buffer) { - os_log_error(log_, - "Error creating AVAudioPCMBuffer with format %{public}@ and frame capacity %d", - stringDescribingAVAudioFormat(renderFormat), ringBufferChunkSize); - decoderState->error_ = [NSError errorWithDomain:SFBAudioPlayerErrorDomain - code:SFBAudioPlayerErrorCodeInternalError - userInfo:nil]; - decoderState->flags_.fetch_or( - static_cast(DecoderState::Flags::cancelRequested), - std::memory_order_acq_rel); - continue; - } - } - } else { - decoderState = nullptr; - } - } - } - - if (decoderState) { - if (const auto flags = flags_.load(std::memory_order_acquire); - !(flags & static_cast(Flags::drainRequired))) { - // Decode and write chunks to the ring buffer - while (audioRingBuffer_.FreeSpace() >= ringBufferChunkSize) { - // Decoding started - if (const auto flags = decoderState->flags_.load(std::memory_order_acquire); - !(flags & static_cast(DecoderState::Flags::decodingStarted))) { - const bool suspended = - flags & static_cast(DecoderState::Flags::decodingSuspended); - - if (!suspended) - os_log_debug(log_, "Decoding starting for %{public}@", decoderState->decoder_); - else - os_log_debug(log_, "Decoding starting after suspension for %{public}@", - decoderState->decoder_); - - decoderState->flags_.fetch_or(static_cast(DecoderState::Flags::decodingStarted), - std::memory_order_acq_rel); - - // Submit the decoding started event for the initial start only - if (!suspended) { - if (decodingEvents_.WriteValues(DecodingEventCommand::started, - nextEventIdentificationNumber(), - decoderState->sequenceNumber_)) - dispatch_semaphore_signal(eventSemaphore_); - else - os_log_fault(log_, "Error writing decoding started event"); - } - } - - // 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->flags_.fetch_or(static_cast(DecoderState::Flags::cancelRequested), - std::memory_order_acq_rel); - goto next_outer_iteration; - } - - // Write the decoded audio to the ring buffer for rendering - const auto framesWritten = audioRingBuffer_.Write(buffer.audioBufferList, buffer.frameLength); - if (framesWritten != buffer.frameLength) - os_log_fault(log_, - "Error writing audio to ring buffer: CXXCoreAudio::AudioRingBuffer::Write failed " - "for %d frames", - buffer.frameLength); - - // Decoding complete - if (const auto flags = decoderState->flags_.load(std::memory_order_acquire); - flags & static_cast(DecoderState::Flags::decodingComplete)) { - const bool resumed = flags & static_cast(DecoderState::Flags::decodingResumed); - - // Submit the decoding complete event for the first completion only - if (!resumed) { - if (decodingEvents_.WriteValues(DecodingEventCommand::complete, - nextEventIdentificationNumber(), - decoderState->sequenceNumber_)) - dispatch_semaphore_signal(eventSemaphore_); - else - os_log_fault(log_, "Error writing decoding complete event"); - } - - if (!resumed) - os_log_debug(log_, "Decoding complete for %{public}@", decoderState->decoder_); - else - os_log_debug(log_, "Decoding complete after resuming for %{public}@", - decoderState->decoder_); - - break; - } - } - - // Clear the mute flag if needed now that the ring buffer is full - if (flags & static_cast(Flags::isMuted)) - flags_.fetch_and(~static_cast(Flags::isMuted), std::memory_order_acq_rel); - } - } - - int64_t deltaNanos; - if (!decoderState) { - // Shorter timeout if waiting on a decoder to complete rendering for a pending format change - if (formatMismatch) - deltaNanos = 25 * NSEC_PER_MSEC; - // Idling - else - deltaNanos = NSEC_PER_SEC / 2; - } 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(); - - // Minimal timeout if the ring buffer has more free space than desired - if (freeSpace > targetMaxFreeSpace) - deltaNanos = 2.5 * NSEC_PER_MSEC; - else { - const auto duration = (targetMaxFreeSpace - freeSpace) / audioRingBuffer_.Format().mSampleRate; - deltaNanos = duration * NSEC_PER_SEC; - } - } - - // Wait for an event signal; ring buffer space availability is polled using the timeout - dispatch_semaphore_wait(decodingSemaphore_, dispatch_time(DISPATCH_TIME_NOW, deltaNanos)); - - next_outer_iteration:; - } - - os_log_debug(log_, " decoding thread complete", this); -} - -void sfb::AudioPlayer::submitDecodingErrorEvent(NSError *error) noexcept { -#if DEBUG - assert(error != nil); -#endif /* DEBUG */ - - NSError *err = nil; - NSData *errorData = [NSKeyedArchiver archivedDataWithRootObject:error requiringSecureCoding:YES error:&err]; - if (!errorData) { - os_log_error(log_, "Error archiving NSError for decoding error event: %{public}@", err); - return; - } - - auto [front, back] = decodingEvents_.GetWriteVector(); - - const auto frontSize = front.size(); - const auto spaceNeeded = sizeof(DecodingEventCommand) + sizeof(uint64_t) + sizeof(uint32_t) + errorData.length; - if (frontSize + back.size() < spaceNeeded) { - os_log_fault(log_, "Insufficient space to write decoding error event"); - return; - } - - std::size_t cursor = 0; - auto write_single_arg = [&](const void *arg, std::size_t len) noexcept { - const auto *src = static_cast(arg); - if (cursor + len <= frontSize) - std::memcpy(front.data() + cursor, src, len); - else if (cursor >= frontSize) - std::memcpy(back.data() + (cursor - frontSize), src, len); - else { - const size_t toFront = frontSize - cursor; - std::memcpy(front.data() + cursor, src, toFront); - std::memcpy(back.data(), src + toFront, len - toFront); - } - cursor += len; - }; - - // Event header and payload - const auto command = DecodingEventCommand::error; - const auto identificationNumber = nextEventIdentificationNumber(); - const auto dataSize = static_cast(errorData.length); - - write_single_arg(&command, sizeof command); - write_single_arg(&identificationNumber, sizeof identificationNumber); - write_single_arg(&dataSize, sizeof dataSize); - write_single_arg(errorData.bytes, errorData.length); - - decodingEvents_.CommitWrite(cursor); - dispatch_semaphore_signal(eventSemaphore_); -} - -// MARK: - Rendering - -OSStatus sfb::AudioPlayer::render(BOOL& isSilence, const AudioTimeStamp& timestamp, AVAudioFrameCount frameCount, - AudioBufferList *outputData) noexcept { - const auto flags = flags_.load(std::memory_order_acquire); - - // Discard any stale frames in the ring buffer from a seek or decoder cancelation - if (flags & static_cast(Flags::drainRequired)) { - audioRingBuffer_.Drain(); - flags_.fetch_and(~static_cast(Flags::drainRequired), std::memory_order_acq_rel); - for (UInt32 i = 0; i < outputData->mNumberBuffers; ++i) - std::memset(outputData->mBuffers[i].mData, 0, outputData->mBuffers[i].mDataByteSize); - isSilence = YES; - return noErr; - } - - // Output silence if not playing or muted - if (constexpr auto mask = static_cast(Flags::isPlaying) | static_cast(Flags::isMuted); - (flags & mask) != static_cast(Flags::isPlaying)) { - for (UInt32 i = 0; i < outputData->mNumberBuffers; ++i) - std::memset(outputData->mBuffers[i].mData, 0, outputData->mBuffers[i].mDataByteSize); - isSilence = YES; - return noErr; - } - - // Read audio from the ring buffer - if (const auto framesRead = audioRingBuffer_.Read(outputData, frameCount); framesRead > 0) { -#if DEBUG - if (framesRead != frameCount) - os_log_debug(log_, "Insufficient audio in ring buffer: %zu frames available, %u requested", framesRead, - frameCount); -#endif /* DEBUG */ - if (!renderingEvents_.WriteValues(RenderingEventCommand::framesRendered, nextEventIdentificationNumber(), - timestamp.mHostTime, timestamp.mRateScalar, - static_cast(framesRead))) - os_log_fault(log_, "Error writing frames rendered event"); - } else { - isSilence = YES; - } - - return noErr; -} - -// MARK: - Event Processing - -<<<<<<< HEAD -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L -void sfb::AudioPlayer::sequenceAndProcessEvents(std::stop_token stoken) noexcept -#else -void sfb::AudioPlayer::sequenceAndProcessEvents() noexcept -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ -{ - pthread_setname_np("AudioPlayer.Events"); - pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); -======= -void sfb::AudioPlayer::sequenceAndProcessEvents(std::stop_token stoken) noexcept { - pthread_setname_np("AudioPlayer.Events"); - pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); ->>>>>>> main - - os_log_debug(log_, " event processing thread starting", this); - -<<<<<<< HEAD - // Returns true if the event processing thread should exit - const auto stop_requested = [&] { -#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - return stoken.stop_requested(); -#else - return (flags_.load(std::memory_order_acquire) & static_cast(Flags::stopEventThread)); -#endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - }; - - while(!stop_requested()) { - DecodingEventCommand decodingEventCommand; - uint64_t decodingEventIdentificationNumber; - auto gotDecodingEvent = decodingEvents_.ReadValues(decodingEventCommand, decodingEventIdentificationNumber); -======= - while (!stoken.stop_requested()) { - DecodingEventCommand decodingEventCommand; - uint64_t decodingEventIdentificationNumber; - auto gotDecodingEvent = decodingEvents_.ReadValues(decodingEventCommand, decodingEventIdentificationNumber); ->>>>>>> main - - RenderingEventCommand renderingEventCommand; - uint64_t renderingEventIdentificationNumber; - auto gotRenderingEvent = renderingEvents_.ReadValues(renderingEventCommand, renderingEventIdentificationNumber); - - // Process all pending decoding and rendering events in sequential order - while (gotDecodingEvent || gotRenderingEvent) { - if (gotDecodingEvent && - (!gotRenderingEvent || decodingEventIdentificationNumber < renderingEventIdentificationNumber)) { - processDecodingEvent(decodingEventCommand); - gotDecodingEvent = decodingEvents_.ReadValues(decodingEventCommand, decodingEventIdentificationNumber); - } else { - processRenderingEvent(renderingEventCommand); - gotRenderingEvent = - renderingEvents_.ReadValues(renderingEventCommand, renderingEventIdentificationNumber); - } - } - - int64_t deltaNanos; - { - std::lock_guard lock{activeDecodersLock_}; - if (firstActiveDecoderState()) - deltaNanos = 7.5 * NSEC_PER_MSEC; - // Use a longer timeout when idle - else - deltaNanos = NSEC_PER_SEC / 2; - } - - // Decoding events will be signaled; render events are polled using the timeout - dispatch_semaphore_wait(eventSemaphore_, dispatch_time(DISPATCH_TIME_NOW, deltaNanos)); - } - - os_log_debug(log_, " event processing thread complete", this); -} - -// MARK: Decoding Events - -bool sfb::AudioPlayer::processDecodingEvent(DecodingEventCommand command) noexcept { - switch (command) { - case DecodingEventCommand::started: - return processDecodingStartedEvent(); - - case DecodingEventCommand::complete: - return processDecodingCompleteEvent(); - - case DecodingEventCommand::canceled: - return processDecoderCanceledEvent(); - - case DecodingEventCommand::error: - return processDecodingErrorEvent(); - - default: - // assert(false && "Unknown decoding event command"); - os_log_error(log_, "Unknown decoding event command: %u", command); - return false; - } -} - -bool sfb::AudioPlayer::processDecodingStartedEvent() noexcept { - uint64_t sequenceNumber; - if (!decodingEvents_.ReadValue(sequenceNumber)) { - os_log_error(log_, "Missing decoder sequence number for decoding started event"); - return false; - } - - Decoder decoder = nil; - Decoder currentDecoder = nil; - { - std::lock_guard lock{activeDecodersLock_}; - - if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); - iter != activeDecoders_.cend()) - decoder = (*iter)->decoder_; - else { - os_log_error(log_, "Decoder state with sequence number %llu missing for decoding started event", - sequenceNumber); - return false; - } - - if (const auto *decoderState = firstActiveDecoderState(); decoderState) - currentDecoder = decoderState->decoder_; - } - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:decodingStarted:)]) - [player_.delegate audioPlayer:player_ decodingStarted:decoder]; - - if (const auto flags = flags_.load(std::memory_order_acquire); - !(flags & static_cast(Flags::isPlaying)) && decoder == currentDecoder) - setNowPlaying(decoder); - - return true; -} - -bool sfb::AudioPlayer::processDecodingCompleteEvent() noexcept { - uint64_t sequenceNumber; - if (!decodingEvents_.ReadValue(sequenceNumber)) { - os_log_error(log_, "Missing decoder sequence number for decoding complete event"); - return false; - } - - Decoder decoder = nil; - { - std::lock_guard lock{activeDecodersLock_}; - - if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); - iter != activeDecoders_.cend()) - decoder = (*iter)->decoder_; - else { - os_log_error(log_, "Decoder state with sequence number %llu missing for decoding complete event", - sequenceNumber); - return false; - } - } - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:decodingComplete:)]) - [player_.delegate audioPlayer:player_ decodingComplete:decoder]; - - return true; -} - -bool sfb::AudioPlayer::processDecoderCanceledEvent() noexcept { - uint64_t sequenceNumber; - if (!decodingEvents_.ReadValue(sequenceNumber)) { - os_log_error(log_, "Missing decoder sequence number for decoder canceled event"); - return false; - } - - Decoder decoder = nil; - NSError *error = nil; - AVAudioFramePosition framesRendered = 0; - { - std::lock_guard lock{activeDecodersLock_}; - - if (const auto iter = std::ranges::find(activeDecoders_, sequenceNumber, &DecoderState::sequenceNumber_); - iter != activeDecoders_.cend()) { - decoder = (*iter)->decoder_; - error = (*iter)->error_; - framesRendered = (*iter)->framesRendered_.load(std::memory_order_acquire); - - os_log_debug(log_, "Deleting decoder state for %{public}@", (*iter)->decoder_); - activeDecoders_.erase(iter); - } else { - os_log_error(log_, "Decoder state with sequence number %llu missing for decoder canceled event", - sequenceNumber); - return false; - } - } - - // Mark the decoder as canceled for any scheduled render notifications - objc_setAssociatedObject(decoder, &decoderIsCanceledKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - - if (!error && [player_.delegate respondsToSelector:@selector(audioPlayer:decoderCanceled:framesRendered:)]) - [player_.delegate audioPlayer:player_ decoderCanceled:decoder framesRendered:framesRendered]; - else if (error && [player_.delegate respondsToSelector:@selector(audioPlayer: - decodingAborted:error:framesRendered:)]) - [player_.delegate audioPlayer:player_ decodingAborted:decoder error:error framesRendered:framesRendered]; - - const auto hasNoDecoders = [&] { - std::scoped_lock lock{queuedDecodersLock_, activeDecodersLock_}; - return queuedDecoders_.empty() && activeDecoders_.empty(); - }(); - - if (hasNoDecoders) { - setNowPlaying(nil); - - const auto didStopEngine = stopEngineIfRunning(); - if (didStopEngine && [player_.delegate respondsToSelector:@selector(audioPlayer:playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ playbackStateChanged:SFBAudioPlayerPlaybackStateStopped]; - } - - return true; -} - -bool sfb::AudioPlayer::processDecodingErrorEvent() noexcept { - // The size in bytes of the archived NSError data - uint32_t dataSize; - if (!decodingEvents_.ReadValue(dataSize)) { - os_log_error(log_, "Missing data size for decoding error event"); - return false; - } - - // The archived NSError data - NSMutableData *data = [NSMutableData dataWithLength:dataSize]; - if (decodingEvents_.Read(data.mutableBytes, 1, dataSize, false) != dataSize) { - os_log_error(log_, "Missing or incomplete archived NSError for decoding error event"); - return false; - } - - NSError *err = nil; - NSError *error = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSError class] fromData:data error:&err]; - if (!error) { - os_log_error(log_, "Error unarchiving NSError for decoding error event: %{public}@", err); - return false; - } - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:encounteredError:)]) - [player_.delegate audioPlayer:player_ encounteredError:error]; - - return true; -} - -// MARK: Rendering Events - -bool sfb::AudioPlayer::processRenderingEvent(RenderingEventCommand command) noexcept { - switch (command) { - case RenderingEventCommand::framesRendered: - return processFramesRenderedEvent(); - - default: - // assert(false && "Unknown rendering event command"); - os_log_error(log_, "Unknown rendering event command: %u", command); - return false; - } -} - -bool sfb::AudioPlayer::processFramesRenderedEvent() noexcept { - // The host time and rate scalar from the render cycle's timestamp - uint64_t hostTime; - double rateScalar; - // The number of valid frames rendered - uint32_t framesRendered; - if (!renderingEvents_.ReadValues(hostTime, rateScalar, framesRendered)) { - os_log_error(log_, "Missing timestamp or frames rendered for frames rendered event"); - return false; - } - -#if DEBUG - assert(framesRendered > 0); -#endif /* DEBUG */ - - // Perform bookkeeping to apportion the rendered frames appropriately - // - // framesRendered contains the number of valid frames that were rendered - // but they could have come from multiple decoders - - struct RenderingEventDetails { - enum class Type { - willStart, - willComplete, - }; - Type type_; - Decoder _Nonnull decoder_; - uint64_t time_; - }; - - // Queued events to be dispatched once the lock is released - std::vector queuedEvents; - - { - std::lock_guard lock{activeDecodersLock_}; - - AVAudioFramePosition framesRemainingToDistribute = framesRendered; - - auto iter = activeDecoders_.cbegin(); - while (iter != activeDecoders_.cend()) { - const auto flags = (*iter)->flags_.load(std::memory_order_acquire); - - // 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 decoderFramesConverted = (*iter)->framesConverted_.load(std::memory_order_acquire); - const auto decoderFramesRendered = (*iter)->framesRendered_.load(std::memory_order_acquire); - const auto decoderFramesRemaining = decoderFramesConverted - 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 (constexpr auto mask = static_cast(DecoderState::Flags::isCanceled) | - static_cast(DecoderState::Flags::renderingStarted); - !(flags & mask)) { - (*iter)->flags_.fetch_or(static_cast(DecoderState::Flags::renderingStarted), - std::memory_order_acq_rel); - - const auto frameOffset = framesRendered - framesRemainingToDistribute; - const double deltaSeconds = frameOffset / (*iter)->sampleRate_; - uint64_t eventTime = - hostTime + host_time::fromNanoseconds(static_cast(deltaSeconds * rateScalar * 1e9)); - - try { - queuedEvents.push_back({RenderingEventDetails::Type::willStart, (*iter)->decoder_, eventTime}); - } catch (const std::exception& e) { - os_log_error(log_, "Error queuing rendering will start event for %{public}@: %{public}s", - (*iter)->decoder_, e.what()); - } - } - - const auto framesFromThisDecoder = std::min(decoderFramesRemaining, framesRemainingToDistribute); - - (*iter)->framesRendered_.fetch_add(framesFromThisDecoder, std::memory_order_acq_rel); - framesRemainingToDistribute -= framesFromThisDecoder; - - // Rendering is complete - if (constexpr auto mask = static_cast(DecoderState::Flags::isCanceled) | - static_cast(DecoderState::Flags::decodingComplete); - (flags & mask) == static_cast(DecoderState::Flags::decodingComplete) && - framesFromThisDecoder == decoderFramesRemaining) { - const auto frameOffset = framesRendered - framesRemainingToDistribute; - const double deltaSeconds = frameOffset / (*iter)->sampleRate_; - uint64_t eventTime = - hostTime + host_time::fromNanoseconds(static_cast(deltaSeconds * rateScalar * 1e9)); - - try { - queuedEvents.push_back({RenderingEventDetails::Type::willComplete, (*iter)->decoder_, eventTime}); - } catch (const std::exception& e) { - os_log_error(log_, "Error queuing rendering will complete event for %{public}@: %{public}s", - (*iter)->decoder_, e.what()); - } - - os_log_debug(log_, "Deleting decoder state for %{public}@", (*iter)->decoder_); - iter = activeDecoders_.erase(iter); - } else { - ++iter; - } - - // All frames processed - if (framesRemainingToDistribute == 0) - break; - } - } - - // Call functions that notify the delegate after unlocking the lock - for (const auto& event : queuedEvents) { - switch (event.type_) { - case RenderingEventDetails::Type::willStart: - handleRenderingWillStartEvent(event.decoder_, event.time_); - break; - case RenderingEventDetails::Type::willComplete: - handleRenderingWillCompleteEvent(event.decoder_, event.time_); - break; - default: - assert(false && "Unknown RenderingEventDetails::Type"); - } - } - - return true; -} - -void sfb::AudioPlayer::handleRenderingWillStartEvent(Decoder decoder, uint64_t hostTime) noexcept { - const auto now = host_time::current(); - if (now > hostTime) - os_log_error(log_, "Rendering started event processed %.2f msec late for %{public}@", - static_cast(host_time::toNanoseconds(now - hostTime)) / 1e6, decoder); -#if DEBUG - else - os_log_debug(log_, "Rendering will start in %.2f msec for %{public}@", - static_cast(host_time::toNanoseconds(hostTime - now)) / 1e6, decoder); -#endif /* DEBUG */ - - // Since the rendering started notification is submitted for asynchronous execution, - // store a weak reference to the owning SFBAudioPlayer to prevent use-after-free - // in the event the player is deallocated before the closure is called - __weak SFBAudioPlayer *weakPlayer = player_; - - // Schedule the rendering started notification at the expected host time - dispatch_after(hostTime, eventQueue_, ^{ - // If weakPlayer is nil it means the SFBAudioPlayer instance was deallocated - __strong SFBAudioPlayer *player = weakPlayer; - if (!player) { - os_log_debug(log_, - "Audio player deallocated between rendering will start and rendering started notifications"); - return; - } - - if (NSNumber *isCanceled = objc_getAssociatedObject(decoder, &decoderIsCanceledKey); isCanceled.boolValue) { - os_log_debug(log_, "%{public}@ canceled after rendering will start notification", decoder); - return; - } - - // Not this, but that: `this` is not safe to use within this closure - auto& that = player->_player; - -#if DEBUG - const auto now = host_time::current(); - const auto delta = host_time::toNanoseconds(absoluteDifference(hostTime, now)); - const auto tolerance = static_cast(1e9 / [that->sourceNode_ outputFormatForBus:0].sampleRate); - if (delta > tolerance) - os_log_debug(log_, "Rendering started notification arrived %.2f msec %s", static_cast(delta) / 1e6, - now > hostTime ? "late" : "early"); -#endif /* DEBUG */ - - that->setNowPlaying(decoder); - - if ([player.delegate respondsToSelector:@selector(audioPlayer:renderingStarted:)]) - [player.delegate audioPlayer:player renderingStarted:decoder]; - }); - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:renderingWillStart:atHostTime:)]) - [player_.delegate audioPlayer:player_ renderingWillStart:decoder atHostTime:hostTime]; -} - -void sfb::AudioPlayer::handleRenderingWillCompleteEvent(Decoder decoder, uint64_t hostTime) noexcept { - const auto now = host_time::current(); - if (now > hostTime) - os_log_error(log_, "Rendering complete event processed %.2f msec late for %{public}@", - static_cast(host_time::toNanoseconds(now - hostTime)) / 1e6, decoder); -#if DEBUG - else - os_log_debug(log_, "Rendering will complete in %.2f msec for %{public}@", - static_cast(host_time::toNanoseconds(hostTime - now)) / 1e6, decoder); -#endif /* DEBUG */ - - // Since the rendering complete notification is submitted for asynchronous execution, - // store a weak reference to the owning SFBAudioPlayer to prevent use-after-free - // in the event the player is deallocated before the closure is called - __weak SFBAudioPlayer *weakPlayer = player_; - - // Schedule the rendering complete notification at the expected host time - dispatch_after(hostTime, eventQueue_, ^{ - // If weakPlayer is nil it means the owning SFBAudioPlayer instance was deallocated - __strong SFBAudioPlayer *player = weakPlayer; - if (!player) { - os_log_debug( - log_, - "Audio player deallocated between rendering will complete and rendering complete notifications"); - return; - } - - if (NSNumber *isCanceled = objc_getAssociatedObject(decoder, &decoderIsCanceledKey); isCanceled.boolValue) { - os_log_debug(log_, "%{public}@ canceled after rendering will complete notification", decoder); - return; - } - - // Not this, but that: `this` is not safe to use within this closure - auto& that = player->_player; - -#if DEBUG - const auto now = host_time::current(); - const auto delta = host_time::toNanoseconds(absoluteDifference(hostTime, now)); - const auto tolerance = static_cast(1e9 / [that->sourceNode_ outputFormatForBus:0].sampleRate); - if (delta > tolerance) - os_log_debug(log_, "Rendering complete notification arrived %.2f msec %s", static_cast(delta) / 1e6, - now > hostTime ? "late" : "early"); -#endif /* DEBUG */ - - if ([player.delegate respondsToSelector:@selector(audioPlayer:renderingComplete:)]) - [player.delegate audioPlayer:player renderingComplete:decoder]; - - const auto hasNoDecoders = [&] { - std::scoped_lock lock{that->queuedDecodersLock_, that->activeDecodersLock_}; - return that->queuedDecoders_.empty() && that->activeDecoders_.empty(); - }(); - - // End of audio - if (hasNoDecoders) { -#if DEBUG - os_log_debug(log_, "End of audio reached"); -#endif /* DEBUG */ - - that->setNowPlaying(nil); - - if ([player.delegate respondsToSelector:@selector(audioPlayerEndOfAudio:)]) - [player.delegate audioPlayerEndOfAudio:player]; - else { - const auto didStopEngine = stopEngineIfRunning(); - if (didStopEngine && [player_.delegate respondsToSelector:@selector(audioPlayer:playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ playbackStateChanged:SFBAudioPlayerPlaybackStateStopped]; - } - } - }); - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:renderingWillComplete:atHostTime:)]) - [player_.delegate audioPlayer:player_ renderingWillComplete:decoder atHostTime:hostTime]; -} - -// MARK: - Active Decoder Management - -void sfb::AudioPlayer::cancelActiveDecoders() noexcept { - std::lock_guard lock{activeDecodersLock_}; - - // Cancel all active decoders - auto signal = false; - for (const auto& decoderState : activeDecoders_) { - if (const auto flags = decoderState->flags_.load(std::memory_order_acquire); - !(flags & static_cast(DecoderState::Flags::isCanceled))) { - decoderState->flags_.fetch_or(static_cast(DecoderState::Flags::cancelRequested), - std::memory_order_acq_rel); - signal = true; - } - } - - // Signal the decoding thread if any cancelations were requested - if (signal) - dispatch_semaphore_signal(decodingSemaphore_); -} - -sfb::AudioPlayer::DecoderState *const sfb::AudioPlayer::firstActiveDecoderState() const noexcept { -#if DEBUG - activeDecodersLock_.assert_owner(); -#endif /* DEBUG */ - - const auto iter = std::ranges::find_if(activeDecoders_, [](const auto& decoderState) { - const auto flags = decoderState->flags_.load(std::memory_order_acquire); - return !(flags & static_cast(DecoderState::Flags::isCanceled)); - }); - if (iter == activeDecoders_.cend()) - return nullptr; - return iter->get(); -} - -// MARK: - AVAudioEngine Notification Handling - -void sfb::AudioPlayer::handleAudioEngineConfigurationChange(AVAudioEngine *engine, NSDictionary *userInfo) noexcept { - if (engine != engine_) { - os_log_error(log_, - "AVAudioEngineConfigurationChangeNotification received for incorrect AVAudioEngine instance"); - return; - } - - // AVAudioEngine posts this notification from a dedicated internal dispatch queue - os_log_debug(log_, "Received AVAudioEngineConfigurationChangeNotification"); - - // The output hardware’s channel count or sample rate changed - { - std::unique_lock lock{engineLock_}; - - // AVAudioEngine stops itself when a configuration change occurs - // Flags::engineIsRunning indicates if the engine was running before the interruption - const auto prevFlags = flags_.fetch_and(~static_cast(Flags::engineIsRunning) & - ~static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - constexpr auto mask = - static_cast(Flags::engineIsRunning) | static_cast(Flags::isPlaying); - const auto prevState = prevFlags & mask; - - AVAudioOutputNode *outputNode = engine_.outputNode; - AVAudioMixerNode *mixerNode = engine_.mainMixerNode; - - AVAudioFormat *outputNodeOutputFormat = [outputNode outputFormatForBus:0]; - AVAudioFormat *mixerNodeOutputFormat = [mixerNode outputFormatForBus:0]; - - // The output node's output format tracks the hardware sample rate and channel count - // To avoid format conversion in both the source-mixer and mixer-output connections, - // set the format for the mixer-output connection to the output node's output format - if (outputNodeOutputFormat.sampleRate != mixerNodeOutputFormat.sampleRate || - outputNodeOutputFormat.channelCount != mixerNodeOutputFormat.channelCount) { -#if DEBUG - if (outputNodeOutputFormat.sampleRate != mixerNodeOutputFormat.sampleRate) - os_log_debug(log_, - "Mismatch between main mixer → output node connection sample rate (%g Hz) and hardware " - "sample rate (%g Hz)", - mixerNodeOutputFormat.sampleRate, outputNodeOutputFormat.sampleRate); - if (outputNodeOutputFormat.channelCount != mixerNodeOutputFormat.channelCount) - os_log_debug(log_, - "Mismatch between main mixer → output node connection channel count (%d) and hardware " - "channel count (%d)", - mixerNodeOutputFormat.channelCount, outputNodeOutputFormat.channelCount); - os_log_debug(log_, "Setting main mixer → output node connection format to %{public}@", - stringDescribingAVAudioFormat(outputNodeOutputFormat)); -#endif /* DEBUG */ - - [engine_ disconnectNodeInput:outputNode bus:0]; - - // Reconnect the mixer and output nodes using the output node's output format - [engine_ connect:mixerNode to:outputNode format:outputNodeOutputFormat]; - - [engine_ prepare]; - } - - // Restart AVAudioEngine if previously running - if (prevState & static_cast(Flags::engineIsRunning)) { - if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { - os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); - lock.unlock(); - if ([player_.delegate respondsToSelector:@selector(audioPlayer:encounteredError:)]) - [player_.delegate audioPlayer:player_ encounteredError:startError]; - return; - } - - // Restore previous playback state - flags_.fetch_or(prevState, std::memory_order_acq_rel); - } - } - - if ([player_.delegate respondsToSelector:@selector(audioPlayerAVAudioEngineConfigurationChange:)]) - [player_.delegate audioPlayerAVAudioEngineConfigurationChange:player_]; -} - -#if TARGET_OS_IPHONE -void sfb::AudioPlayer::handleAudioSessionInterruption(NSDictionary *userInfo) noexcept { - const auto interruptionType = [[userInfo objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; - switch (interruptionType) { - case AVAudioSessionInterruptionTypeBegan: { - os_log_debug(log_, "Received AVAudioSessionInterruptionNotification (AVAudioSessionInterruptionTypeBegan)"); - - { - std::lock_guard lock{engineLock_}; - const auto prevFlags = flags_.fetch_and(~static_cast(Flags::engineIsRunning) & - ~static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - constexpr auto mask = - static_cast(Flags::engineIsRunning) | static_cast(Flags::isPlaying); - preInterruptState_ = prevFlags & mask; - } - - if (preInterruptState_ && [player_.delegate respondsToSelector:@selector(audioPlayer:playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ playbackStateChanged:SFBAudioPlayerPlaybackStateStopped]; - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:audioSessionInterruption:)]) - [player_.delegate audioPlayer:player_ audioSessionInterruption:userInfo]; - - break; - } - - case AVAudioSessionInterruptionTypeEnded: { - os_log_debug(log_, "Received AVAudioSessionInterruptionNotification (AVAudioSessionInterruptionTypeEnded)"); - - if ([player_.delegate respondsToSelector:@selector(audioPlayer:audioSessionInterruption:)]) - [player_.delegate audioPlayer:player_ audioSessionInterruption:userInfo]; - - if (const auto interruptionOption = - [[userInfo objectForKey:AVAudioSessionInterruptionOptionKey] unsignedIntegerValue]; - !(interruptionOption & AVAudioSessionInterruptionOptionShouldResume)) - return; - - if (NSError *sessionError = nil; ![[AVAudioSession sharedInstance] setActive:YES error:&sessionError]) { - os_log_error(log_, "Error activating AVAudioSession: %{public}@", sessionError); - if ([player_.delegate respondsToSelector:@selector(audioPlayer:encounteredError:)]) - [player_.delegate audioPlayer:player_ encounteredError:sessionError]; - return; - } - - { - std::unique_lock lock{engineLock_}; - - if (preInterruptState_ & static_cast(Flags::engineIsRunning)) { - if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { - os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); - lock.unlock(); - if ([player_.delegate respondsToSelector:@selector(audioPlayer:encounteredError:)]) - [player_.delegate audioPlayer:player_ encounteredError:startError]; - return; - } - } - - const auto prevFlags = flags_.fetch_or(preInterruptState_, std::memory_order_acq_rel); - constexpr auto mask = - static_cast(Flags::engineIsRunning) | static_cast(Flags::isPlaying); - assert((prevFlags & mask) != static_cast(Flags::isPlaying)); - } - - if (preInterruptState_ && [player_.delegate respondsToSelector:@selector(audioPlayer:playbackStateChanged:)]) - [player_.delegate audioPlayer:player_ - playbackStateChanged:static_cast(preInterruptState_)]; - - break; - } - - default: - os_log_error(log_, "Unknown value %lu for AVAudioSessionInterruptionTypeKey", - static_cast(interruptionType)); - break; - } -} -#endif - -// MARK: - Processing Graph Management - -bool sfb::AudioPlayer::stopEngineIfRunning() noexcept { - std::lock_guard lock{engineLock_}; - if (!engine_.isRunning) - return false; - [engine_ stop]; - flags_.fetch_and(~static_cast(Flags::engineIsRunning) & ~static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - return true; -} - -bool sfb::AudioPlayer::configureProcessingGraphAndRingBufferForFormat(AVAudioFormat *format, NSError **error) noexcept { -#if DEBUG - assert(format != nil); - assert(format.isStandard); - assert(![[sourceNode_ outputFormatForBus:0] isEqual:format]); -#endif /* DEBUG */ - - os_log_debug(log_, "Reconfiguring audio processing graph for %{public}@", stringDescribingAVAudioFormat(format)); - - std::lock_guard lock{engineLock_}; - - // Even if the engine isn't running, call -stop to force release of any render resources - // This is necessary when transitioning between formats with different channel counts - [engine_ stop]; - - // Attempt to preserve the playback state - const auto prevFlags = flags_.fetch_and(~static_cast(Flags::engineIsRunning) & - ~static_cast(Flags::isPlaying), - std::memory_order_acq_rel); - constexpr auto mask = - static_cast(Flags::engineIsRunning) | static_cast(Flags::isPlaying); - const auto prevState = prevFlags & mask; - - // Reconfigure the processing graph - AVAudioConnectionPoint *sourceNodeOutputConnectionPoint = [[engine_ outputConnectionPointsForNode:sourceNode_ - outputBus:0] firstObject]; - [engine_ disconnectNodeOutput:sourceNode_]; - - // Allocate the ring buffer for the new format - if (!audioRingBuffer_.Allocate(*(format.streamDescription), ringBufferCapacity)) { - os_log_error(log_, - "Unable to create audio ring buffer: CXXCoreAudio::AudioRingBuffer::Allocate failed with format " - "%{public}@ and capacity %zu", - CXXCoreAudio::AudioStreamBasicDescriptionFormatDescription(*(format.streamDescription)), - ringBufferCapacity); - if (error) - *error = [NSError errorWithDomain:SFBAudioPlayerErrorDomain - code:SFBAudioPlayerErrorCodeInternalError - userInfo:nil]; - return false; - } - - // 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 - // have been inserted between the source and mixer nodes. In this case allow the delegate - // to make any necessary adjustments based on the format change if desired. - if (AVAudioMixerNode *mixerNode = engine_.mainMixerNode; - sourceNodeOutputConnectionPoint && sourceNodeOutputConnectionPoint.node != mixerNode) { - if ([player_.delegate respondsToSelector:@selector(audioPlayer:reconfigureProcessingGraph:withFormat:)]) { - AVAudioNode *node = [player_.delegate audioPlayer:player_ - reconfigureProcessingGraph:engine_ - withFormat:format]; - // Ensure the delegate returned a valid node - assert(node != nil && "nil AVAudioNode returned by -audioPlayer:reconfigureProcessingGraph:withFormat:"); - assert([engine_ inputConnectionPointForNode:engine_.outputNode inputBus:0].node == mixerNode && - "Illegal AVAudioEngine configuration"); - [engine_ connect:sourceNode_ to:node format:format]; - } else { - [engine_ connect:sourceNode_ to:sourceNodeOutputConnectionPoint.node format:format]; - } - } else { - [engine_ connect:sourceNode_ to:mixerNode format:format]; - } - -#if DEBUG - logProcessingGraphDescription(log_, OS_LOG_TYPE_DEBUG); -#endif /* DEBUG */ - - [engine_ prepare]; - - // Restart AVAudioEngine and playback as appropriate - if (prevState & static_cast(Flags::engineIsRunning)) { - if (NSError *startError = nil; ![engine_ startAndReturnError:&startError]) { - os_log_error(log_, "Error starting AVAudioEngine: %{public}@", startError); - // TODO: Re-evaluate whether failure to start AVAudioEngine during reconfiguration - // should be treated as a fatal error or handled as a recoverable condition, - // and document the chosen and tested behavior. - if (error) - *error = startError; - return false; - } - - flags_.fetch_or(prevState, std::memory_order_acq_rel); - } - - return true; -} From b1ccf5c4f6647353d448dcc59b1eff65329ef37d Mon Sep 17 00:00:00 2001 From: Stephen Booth Date: Thu, 22 Jan 2026 11:12:52 -0600 Subject: [PATCH 9/9] Reformat --- Sources/CSFBAudioEngine/Player/AudioPlayer.h | 60 +++++----- Sources/CSFBAudioEngine/Player/AudioPlayer.mm | 113 +++++++++--------- 2 files changed, 85 insertions(+), 88 deletions(-) diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.h b/Sources/CSFBAudioEngine/Player/AudioPlayer.h index c9f93e174..0309e30e5 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.h +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.h @@ -67,21 +67,21 @@ class AudioPlayer final { /// Thread used for decoding #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - std::jthread decodingThread_; + std::jthread decodingThread_; #else - std::thread decodingThread_; + std::thread decodingThread_; #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - /// Dispatch semaphore used for communication with the decoding thread - dispatch_semaphore_t decodingSemaphore_ {nil}; + /// Dispatch semaphore used for communication with the decoding thread + dispatch_semaphore_t decodingSemaphore_{nil}; - /// Thread used for event processing + /// Thread used for event processing #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - std::jthread eventThread_; + std::jthread eventThread_; #else - std::thread eventThread_; + std::thread eventThread_; #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - /// Dispatch semaphore used for communication with the event processing thread - dispatch_semaphore_t eventSemaphore_ {nil}; + /// Dispatch semaphore used for communication with the event processing thread + dispatch_semaphore_t eventSemaphore_{nil}; /// Ring buffer communicating events from the decoding thread to the event processing thread CXXRingBuffer::RingBuffer decodingEvents_; @@ -201,32 +201,32 @@ class AudioPlayer final { void logProcessingGraphDescription(os_log_t _Nonnull log, os_log_type_t type) const noexcept; private: - /// Possible bits in `flags_` - enum class Flags : unsigned int { - /// Cached value of `engine_.isRunning` - engineIsRunning = 1u << 0, - /// The render block should output audio - isPlaying = 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, + /// Possible bits in `flags_` + enum class Flags : unsigned int { + /// Cached value of `engine_.isRunning` + engineIsRunning = 1u << 0, + /// The render block should output audio + isPlaying = 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, #if !(defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L) - /// The decoding thread should exit - stopDecodingThread = 1u << 4, - /// The event thread should exit - stopEventThread = 1u << 5, + /// The decoding thread should exit + stopDecodingThread = 1u << 4, + /// The event thread should exit + stopEventThread = 1u << 5, #endif /* !(defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L) */ - }; + }; // MARK: - Decoding /// Dequeues and processes decoders from the decoder queue - /// - note: This is the thread entry point for the decoding thread + /// - note: This is the thread entry point for the decoding thread #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - void processDecoders(std::stop_token stoken) noexcept; + void processDecoders(std::stop_token stoken) noexcept; #else - void processDecoders() noexcept; + void processDecoders() noexcept; #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ /// Writes an error event to `decodingEvents_` and signals `eventSemaphore_` @@ -261,11 +261,11 @@ class AudioPlayer final { // MARK: - Event Processing /// Reads and sequences event headers from `decodingEvents_` and `renderingEvents_` for processing in order - /// - note: This is the thread entry point for the event processing thread + /// - note: This is the thread entry point for the event processing thread #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - void sequenceAndProcessEvents(std::stop_token stoken) noexcept; + void sequenceAndProcessEvents(std::stop_token stoken) noexcept; #else - void sequenceAndProcessEvents() noexcept; + void sequenceAndProcessEvents() noexcept; #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ /// Reads and processes an event payload from `decodingEvents_` diff --git a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm index 7b1a60552..a760de4a6 100644 --- a/Sources/CSFBAudioEngine/Player/AudioPlayer.mm +++ b/Sources/CSFBAudioEngine/Player/AudioPlayer.mm @@ -411,18 +411,18 @@ constexpr T absoluteDifference(T a, T b) noexcept { } // Launch the decoding and event processing threads - try { + try { #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - decodingThread_ = std::jthread(std::bind_front(&sfb::AudioPlayer::processDecoders, this)); - eventThread_ = std::jthread(std::bind_front(&sfb::AudioPlayer::sequenceAndProcessEvents, this)); + decodingThread_ = std::jthread(std::bind_front(&sfb::AudioPlayer::processDecoders, this)); + eventThread_ = std::jthread(std::bind_front(&sfb::AudioPlayer::sequenceAndProcessEvents, this)); #else - decodingThread_ = std::thread(&sfb::AudioPlayer::processDecoders, this); - eventThread_ = std::thread(&sfb::AudioPlayer::sequenceAndProcessEvents, this); + decodingThread_ = std::thread(&sfb::AudioPlayer::processDecoders, this); + eventThread_ = std::thread(&sfb::AudioPlayer::sequenceAndProcessEvents, this); #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - } catch(const std::exception& e) { - os_log_error(log_, "Unable to create thread: %{public}s", e.what()); - throw; - } + } catch (const std::exception& e) { + os_log_error(log_, "Unable to create thread: %{public}s", e.what()); + throw; + } // ======================================== // Audio Processing Graph Setup @@ -480,45 +480,42 @@ constexpr T absoluteDifference(T a, T b) noexcept { cancelActiveDecoders(); #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - // Register a stop callback for the decoding thread - std::stop_callback decodingThreadStopCallback(decodingThread_.get_stop_token(), [this] { - dispatch_semaphore_signal(decodingSemaphore_); - }); + // Register a stop callback for the decoding thread + std::stop_callback decodingThreadStopCallback(decodingThread_.get_stop_token(), + [this] { dispatch_semaphore_signal(decodingSemaphore_); }); - // Issue a stop request to the decoding thread and wait for it to exit - decodingThread_.request_stop(); + // Issue a stop request to the decoding thread and wait for it to exit + decodingThread_.request_stop(); #else - // Stop the decoding thread - flags_.fetch_or(static_cast(Flags::stopDecodingThread), std::memory_order_acq_rel); - dispatch_semaphore_signal(decodingSemaphore_); + // Stop the decoding thread + flags_.fetch_or(static_cast(Flags::stopDecodingThread), std::memory_order_acq_rel); + dispatch_semaphore_signal(decodingSemaphore_); #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - try { - decodingThread_.join(); - } catch(const std::exception& e) { - os_log_error(log_, "Unable to join decoding thread: %{public}s", e.what()); - } - + try { + decodingThread_.join(); + } catch (const std::exception& e) { + os_log_error(log_, "Unable to join decoding thread: %{public}s", e.what()); + } #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - // Register a stop callback for the event processing thread - std::stop_callback eventThreadStopCallback(eventThread_.get_stop_token(), [this] { - dispatch_semaphore_signal(eventSemaphore_); - }); + // Register a stop callback for the event processing thread + std::stop_callback eventThreadStopCallback(eventThread_.get_stop_token(), + [this] { dispatch_semaphore_signal(eventSemaphore_); }); - // Issue a stop request to the event processing thread and wait for it to exit - eventThread_.request_stop(); + // Issue a stop request to the event processing thread and wait for it to exit + eventThread_.request_stop(); #else - // Stop the event processing thread - flags_.fetch_or(static_cast(Flags::stopEventThread), std::memory_order_acq_rel); - dispatch_semaphore_signal(eventSemaphore_); + // Stop the event processing thread + flags_.fetch_or(static_cast(Flags::stopEventThread), std::memory_order_acq_rel); + dispatch_semaphore_signal(eventSemaphore_); #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - try { - eventThread_.join(); - } catch(const std::exception& e) { - os_log_error(log_, "Unable to join event processing thread: %{public}s", e.what()); - } + try { + eventThread_.join(); + } catch (const std::exception& e) { + os_log_error(log_, "Unable to join event processing thread: %{public}s", e.what()); + } // Delete any remaining decoder state activeDecoders_.clear(); @@ -1014,8 +1011,8 @@ constexpr T absoluteDifference(T a, T b) noexcept { void sfb::AudioPlayer::processDecoders() noexcept #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ { - pthread_setname_np("AudioPlayer.Decoding"); - pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); + pthread_setname_np("AudioPlayer.Decoding"); + pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); os_log_debug(log_, " decoding thread starting", this); @@ -1025,18 +1022,18 @@ constexpr T absoluteDifference(T a, T b) noexcept { auto formatMismatch = false; // Returns true if the decoding thread should exit - const auto stop_requested = [&] { + const auto stop_requested = [&] { #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - return stoken.stop_requested(); + return stoken.stop_requested(); #else - return (flags_.load(std::memory_order_acquire) & static_cast(Flags::stopDecodingThread)); + return (flags_.load(std::memory_order_acquire) & static_cast(Flags::stopDecodingThread)); #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - }; + }; - while(!stop_requested()) { - // The decoder state being processed - DecoderState *decoderState = nullptr; - auto ringBufferStale = false; + while (!stop_requested()) { + // The decoder state being processed + DecoderState *decoderState = nullptr; + auto ringBufferStale = false; { std::lock_guard lock{activeDecodersLock_}; @@ -1509,24 +1506,24 @@ constexpr T absoluteDifference(T a, T b) noexcept { void sfb::AudioPlayer::sequenceAndProcessEvents() noexcept #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ { - pthread_setname_np("AudioPlayer.Events"); - pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); + pthread_setname_np("AudioPlayer.Events"); + pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0); os_log_debug(log_, " event processing thread starting", this); // Returns true if the event processing thread should exit - const auto stop_requested = [&] { + const auto stop_requested = [&] { #if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L - return stoken.stop_requested(); + return stoken.stop_requested(); #else - return (flags_.load(std::memory_order_acquire) & static_cast(Flags::stopEventThread)); + return (flags_.load(std::memory_order_acquire) & static_cast(Flags::stopEventThread)); #endif /* defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L */ - }; + }; - while(!stop_requested()) { - DecodingEventCommand decodingEventCommand; - uint64_t decodingEventIdentificationNumber; - auto gotDecodingEvent = decodingEvents_.ReadValues(decodingEventCommand, decodingEventIdentificationNumber); + while (!stop_requested()) { + DecodingEventCommand decodingEventCommand; + uint64_t decodingEventIdentificationNumber; + auto gotDecodingEvent = decodingEvents_.ReadValues(decodingEventCommand, decodingEventIdentificationNumber); RenderingEventCommand renderingEventCommand; uint64_t renderingEventIdentificationNumber;