|
| 1 | +#pragma once |
| 2 | + |
| 3 | +#include "AudioTools/CoreAudio/AudioPlayer.h" |
| 4 | + |
| 5 | +namespace audio_tools { |
| 6 | +/// Control AudioPlayer command types processed in copy() |
| 7 | + |
| 8 | +enum class AudioPlayerCommandType { |
| 9 | + Begin, |
| 10 | + End, |
| 11 | + Next, |
| 12 | + SetIndex, |
| 13 | + SetPath, |
| 14 | + SetVolume, |
| 15 | + SetMuted, |
| 16 | + SetActive |
| 17 | +}; |
| 18 | + |
| 19 | +struct AudioPlayerCommand { |
| 20 | + AudioPlayerCommandType type; |
| 21 | + int index = 0; // begin/setIndex |
| 22 | + bool isActive = true; // begin/setActive |
| 23 | + int offset = 1; // next |
| 24 | + float volume = 0.0f; // setVolume |
| 25 | + bool muted = false; // setMuted |
| 26 | +}; |
| 27 | + |
| 28 | +/** |
| 29 | + * @class AudioPlayerThreadSafe |
| 30 | + * @brief Lock-free asynchronous control wrapper for AudioPlayer using a command |
| 31 | + * queue. |
| 32 | + * |
| 33 | + * Purpose |
| 34 | + * Provides a minimal, thread-safe control surface (begin, end, next, setIndex, |
| 35 | + * setPath, setVolume, setMuted, setActive) by enqueuing commands from any task |
| 36 | + * and applying them inside copy() in the audio/render thread. This serializes |
| 37 | + * all state changes without a mutex. |
| 38 | + * |
| 39 | + * Contract |
| 40 | + * - Input: Reference to an existing AudioPlayer instance + queue capacity. |
| 41 | + * - API calls: enqueue a Command (non-blocking if underlying queue is |
| 42 | + * configured with zero read/write wait). No direct state mutation happens in |
| 43 | + * the caller's context. |
| 44 | + * - Execution: copy()/copy(bytes) drains the queue first (processCommands()) |
| 45 | + * then performs audio transfer via AudioPlayer::copy(). Order is preserved |
| 46 | + * (FIFO). |
| 47 | + * - Errors: enqueue() returns false if the queue is full (command dropped). |
| 48 | + * Caller may retry later. No blocking is performed by this wrapper. Dequeue in |
| 49 | + * processCommands() assumes the queue's read wait is non-blocking (e.g. |
| 50 | + * QueueRTOS.setReadMaxWait(0)). |
| 51 | + * - Path lifetime: setPath(const char*) stores the pointer; caller must keep |
| 52 | + * the memory valid until the command is consumed. If the path buffer is |
| 53 | + * ephemeral, allocate/copy it externally or extend the Command to own storage |
| 54 | + * (future enhancement). |
| 55 | + * |
| 56 | + * Thread-safety model |
| 57 | + * - All public control methods are producer-only; they never touch the |
| 58 | + * AudioPlayer directly. |
| 59 | + * - The audio thread (calling copy()) is the single consumer applying changes, |
| 60 | + * preventing races. |
| 61 | + * - No mutexes or locks are used; correctness relies on queue's internal |
| 62 | + * synchronization. |
| 63 | + * |
| 64 | + * Callback / reentrancy guidance |
| 65 | + * - Avoid calling wrapper control methods from callbacks invoked by copy() |
| 66 | + * (e.g. EOF callbacks) to prevent immediate feedback loops; schedule such |
| 67 | + * actions from another task. |
| 68 | + * |
| 69 | + * Template parameter |
| 70 | + * - QueueT: a queue class template <class T> providing: constructor(int |
| 71 | + * size,...), bool enqueue(T&), bool dequeue(T&). Example: QueueRTOS. |
| 72 | + * |
| 73 | + * @tparam QueueT Queue class template taking a single type parameter (the |
| 74 | + * command type). |
| 75 | + * @ingroup concurrency |
| 76 | + */ |
| 77 | +template <template <class> class QueueT> |
| 78 | +class AudioPlayerThreadSafe { |
| 79 | + public: |
| 80 | + /** |
| 81 | + * @brief Construct an async-control wrapper around an AudioPlayer. |
| 82 | + * @param p Underlying AudioPlayer to protect |
| 83 | + * @param queueSize Capacity of the internal command queue |
| 84 | + */ |
| 85 | + AudioPlayerThreadSafe(AudioPlayer& p, QueueT<AudioPlayerCommand>& queue) |
| 86 | + : player(p), queue(queue) {} |
| 87 | + |
| 88 | + // Control API: enqueue only; applied in copy() |
| 89 | + bool begin(int index = 0, bool isActive = true) { |
| 90 | + AudioPlayerCommand c{AudioPlayerCommandType::Begin}; |
| 91 | + c.index = index; |
| 92 | + c.isActive = isActive; |
| 93 | + return enqueue(c); |
| 94 | + } |
| 95 | + |
| 96 | + void end() { |
| 97 | + AudioPlayerCommand c{AudioPlayerCommandType::End}; |
| 98 | + enqueue(c); |
| 99 | + } |
| 100 | + |
| 101 | + bool next(int offset = 1) { |
| 102 | + AudioPlayerCommand c{AudioPlayerCommandType::Next}; |
| 103 | + c.offset = offset; |
| 104 | + return enqueue(c); |
| 105 | + } |
| 106 | + |
| 107 | + bool setIndex(int idx) { |
| 108 | + AudioPlayerCommand c{AudioPlayerCommandType::SetIndex}; |
| 109 | + c.index = idx; |
| 110 | + return enqueue(c); |
| 111 | + } |
| 112 | + |
| 113 | + bool setPath(const char* path) { |
| 114 | + AudioPlayerCommand c{AudioPlayerCommandType::SetPath}; |
| 115 | + this->path = path; |
| 116 | + return enqueue(c); |
| 117 | + } |
| 118 | + |
| 119 | + size_t copy() { |
| 120 | + if (queue.size() > 0) processCommands(); |
| 121 | + return player.copy(); |
| 122 | + } |
| 123 | + |
| 124 | + size_t copy(size_t bytes) { |
| 125 | + if (queue.size() > 0) processCommands(); |
| 126 | + return player.copy(bytes); |
| 127 | + } |
| 128 | + |
| 129 | + void setActive(bool active) { |
| 130 | + AudioPlayerCommand c{AudioPlayerCommandType::SetActive}; |
| 131 | + c.isActive = active; |
| 132 | + enqueue(c); |
| 133 | + } |
| 134 | + |
| 135 | + bool setVolume(float v) { |
| 136 | + AudioPlayerCommand c{AudioPlayerCommandType::SetVolume}; |
| 137 | + c.volume = v; |
| 138 | + return enqueue(c); |
| 139 | + } |
| 140 | + |
| 141 | + bool setMuted(bool muted) { |
| 142 | + AudioPlayerCommand c{AudioPlayerCommandType::SetMuted}; |
| 143 | + c.muted = muted; |
| 144 | + return enqueue(c); |
| 145 | + } |
| 146 | + |
| 147 | + private: |
| 148 | + AudioPlayer& player; |
| 149 | + // Internal command queue |
| 150 | + QueueT<AudioPlayerCommand>& queue; |
| 151 | + Str path; |
| 152 | + |
| 153 | + // Drain command queue and apply to underlying player |
| 154 | + void processCommands() { |
| 155 | + AudioPlayerCommand cmd; |
| 156 | + // Attempt non-blocking dequeue loop; requires queue configured non-blocking |
| 157 | + while (dequeue(cmd)) { |
| 158 | + switch (cmd.type) { |
| 159 | + case AudioPlayerCommandType::Begin: |
| 160 | + player.begin(cmd.index, cmd.isActive); |
| 161 | + break; |
| 162 | + case AudioPlayerCommandType::End: |
| 163 | + player.end(); |
| 164 | + break; |
| 165 | + case AudioPlayerCommandType::Next: |
| 166 | + player.next(cmd.offset); |
| 167 | + break; |
| 168 | + case AudioPlayerCommandType::SetIndex: |
| 169 | + player.setIndex(cmd.index); |
| 170 | + break; |
| 171 | + case AudioPlayerCommandType::SetPath: |
| 172 | + player.setPath(path.c_str()); |
| 173 | + break; |
| 174 | + case AudioPlayerCommandType::SetVolume: |
| 175 | + player.setVolume(cmd.volume); |
| 176 | + break; |
| 177 | + case AudioPlayerCommandType::SetMuted: |
| 178 | + player.setMuted(cmd.muted); |
| 179 | + break; |
| 180 | + case AudioPlayerCommandType::SetActive: |
| 181 | + player.setActive(cmd.isActive); |
| 182 | + break; |
| 183 | + } |
| 184 | + if (queue.size() == 0) break; |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + // Queue facade wrappers to allow both internal/external queues |
| 189 | + bool enqueue(AudioPlayerCommand& c) { return queue.enqueue(c); } |
| 190 | + |
| 191 | + bool dequeue(AudioPlayerCommand& c) { return queue.dequeue(c); } |
| 192 | +}; |
| 193 | + |
| 194 | +} // namespace audio_tools |
0 commit comments