Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/sbooth/AVFAudioExtensions", .upToNextMinor(from: "0.5.1")),
.package(url: "https://github.com/sbooth/CXXAudioRingBuffer", .upToNextMinor(from: "0.1.1")),
.package(url: "https://github.com/sbooth/CXXAudioRingBuffer", .upToNextMinor(from: "0.2.0")),
.package(url: "https://github.com/sbooth/CXXDispatchSemaphore", .upToNextMinor(from: "0.4.1")),
.package(url: "https://github.com/sbooth/CXXMessageQueue", .upToNextMinor(from: "0.2.0")),
.package(url: "https://github.com/sbooth/CXXQueue", .upToNextMinor(from: "0.1.1")),
.package(url: "https://github.com/sbooth/CXXUnfairLock", .upToNextMinor(from: "0.3.1")),

// Standalone dependencies from source
Expand Down Expand Up @@ -66,6 +67,7 @@ let package = Package(
.product(name: "CXXAudioRingBuffer", package: "CXXAudioRingBuffer"),
.product(name: "CXXDispatchSemaphore", package: "CXXDispatchSemaphore"),
.product(name: "CXXMessageQueue", package: "CXXMessageQueue"),
.product(name: "CXXQueue", package: "CXXQueue"),
.product(name: "CXXUnfairLock", package: "CXXUnfairLock"),
// Standalone dependencies
.product(name: "dumb", package: "CDUMB"),
Expand Down
62 changes: 50 additions & 12 deletions Sources/CSFBAudioEngine/Player/AudioPlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#import <mpsc/MessageQueue.hpp>
#import <mtx/UnfairMutex.hpp>
#import <spsc/AudioRingBuffer.hpp>
#import <spsc/Queue.hpp>

#import <AVFAudio/AVFAudio.h>

Expand All @@ -34,6 +35,33 @@

namespace sfb {

namespace detail {

/// A descriptor for a decoded chunk of audio.
struct DecodedChunkDescriptor final {
/// The playback generation at the time this chunk was decoded
uint64_t playbackGeneration_{0};
/// Decoder sequence number that produced the audio.
uint64_t sequenceNumber_{0};
/// Decoder frame position for the first audio frame in the chunk.
int64_t framePosition_{0};
/// Number of audio frames in the chunk.
uint32_t frameLength_{0};
};

/// A descriptor for a rendering chunk of audio.
struct RenderingChunkDescriptor final {
/// The decoded chunk descriptor.
DecodedChunkDescriptor descriptor_{};
/// The number of frames consumed from `descriptor_`
uint32_t framesConsumed_{0};

/// Returns the number of frames remaining in this chunk
[[nodiscard]] uint32_t framesRemaining() const noexcept { return descriptor_.frameLength_ - framesConsumed_; }
};

} /* namespace detail */

// MARK: - AudioPlayer

/// SFBAudioPlayer implementation
Expand All @@ -54,7 +82,12 @@ class AudioPlayer final {
using DecoderStateVector = std::vector<std::unique_ptr<DecoderState>>;

/// Ring buffer transferring audio between the decoding thread and the render block
spsc::AudioRingBuffer audioRingBuffer_;
spsc::AudioRingBuffer audioBuffer_;
/// Queue transferring audio metadata between the decoding thread and the render block
spsc::Queue<detail::DecodedChunkDescriptor, 32> audioMetadata_;
/// The current transport epoch
std::atomic<uint64_t> playbackGeneration_{1};
static_assert(std::atomic<uint64_t>::is_always_lock_free, "Lock-free std::atomic<uint64_t> required");

/// Active decoders and associated state
DecoderStateVector activeDecoders_;
Expand Down Expand Up @@ -200,13 +233,13 @@ class AudioPlayer final {
/// Possible bits in `flags_`
enum class Flags : unsigned int {
/// Cached value of `engine_.isRunning`
engineIsRunning = 1u << 0,
engineRunning = 1u << 0,
/// The render block should output audio
isPlaying = 1u << 1,
playing = 1u << 1,
/// The render block should output silence
isMuted = 1u << 2,
/// The ring buffer needs to be drained during the next render cycle
drainRequired = 1u << 3,
muted = 1u << 2,
/// The ring buffer contains stale audio and needs to be emptied during the next render cycle
audioStale = 1u << 3,
/// The event message queue had insufficient space to record a render event
renderEventDropped = 1u << 4,
};
Expand Down Expand Up @@ -248,7 +281,9 @@ class AudioPlayer final {

/// Render block implementation
OSStatus render(BOOL &isSilence, const AudioTimeStamp &timestamp, AVAudioFrameCount frameCount,
AudioBufferList *_Nonnull outputData) noexcept;
AudioBufferList &outputData) noexcept;
/// The current rendering chunk descriptor
detail::RenderingChunkDescriptor renderingChunk_{};

// MARK: - Events

Expand Down Expand Up @@ -311,6 +346,9 @@ class AudioPlayer final {
/// Returns the first decoder state in `activeDecoders_` that has not been canceled
DecoderState *_Nullable firstActiveDecoderState() const noexcept;

/// Returns the decoder state in `activeDecoders_` with the specified sequence number
DecoderState *_Nullable decoderStateWithSequenceNumber(uint64_t sequenceNumber) const noexcept;

public:
// MARK: - AVAudioEngine Notification Handling

Expand Down Expand Up @@ -350,26 +388,26 @@ inline bool AudioPlayer::decoderQueueIsEmpty() const noexcept {

inline SFBAudioPlayerPlaybackState AudioPlayer::playbackState() const noexcept {
const auto flags = loadFlags();
const auto state = flags & (Flags::engineIsRunning | Flags::isPlaying);
const auto state = flags & (Flags::engineRunning | Flags::playing);
#if DEBUG
assert(bits::is_set_or_is_clear(state, Flags::engineIsRunning, Flags::isPlaying));
assert(bits::is_set_or_is_clear(state, Flags::engineRunning, Flags::playing));
#endif /* DEBUG */
return static_cast<SFBAudioPlayerPlaybackState>(state);
}

inline bool AudioPlayer::isPlaying() const noexcept {
const auto flags = loadFlags();
return bits::has_all(flags, Flags::engineIsRunning | Flags::isPlaying);
return bits::has_all(flags, Flags::engineRunning | Flags::playing);
}

inline bool AudioPlayer::isPaused() const noexcept {
const auto flags = loadFlags();
return bits::is_set_and_is_clear(flags, Flags::engineIsRunning, Flags::isPlaying);
return bits::is_set_and_is_clear(flags, Flags::engineRunning, Flags::playing);
}

inline bool AudioPlayer::isStopped() const noexcept {
const auto flags = loadFlags();
return bits::is_clear(flags, Flags::engineIsRunning);
return bits::is_clear(flags, Flags::engineRunning);
}

inline bool AudioPlayer::isReady() const noexcept {
Expand Down
Loading