Skip to content

Commit a2d6d9a

Browse files
committed
AudioPlayerThreadSafe
1 parent d13777d commit a2d6d9a

File tree

7 files changed

+280
-28
lines changed

7 files changed

+280
-28
lines changed

examples/examples-communication/dlna/dlna-audio-renderer/dlna-audio-renderer.ino

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@
88
#include "AudioTools/Disk/AudioSourceURL.h"
99
#include "AudioTools/AudioCodecs/CodecHelix.h"
1010
#include "AudioTools/Concurrency/RTOS.h"
11+
#include "AudioTools/Concurrency/AudioPlayerThreadSafe.h"
1112

12-
const char* ssid = "ssid";
13-
const char* password = "password";
13+
const char* ssid = "Phil Schatzmann";
14+
const char* password = "sabrina01";
1415

1516
// DLNA
1617
const int port = 9000;
1718
WiFiServer wifi(port);
1819
HttpServer<WiFiClient, WiFiServer> server(wifi);
1920
UDPService<WiFiUDP> udp;
2021
DLNAMediaRenderer<WiFiClient> media_renderer(server, udp);
21-
Task dlna_task("dlna", 8000, 10, 0);
2222

2323
// AudioPlayer
2424
URLStream url;
@@ -29,11 +29,16 @@ AACDecoderHelix dec_aac;
2929
MP3DecoderHelix dec_mp3;
3030
WAVDecoder dec_wav;
3131
AudioPlayer player(source, i2s, multi_decoder);
32-
// Callback when playback reaches EOF
32+
33+
// Multitasking
34+
QueueRTOS<AudioPlayerCommand> queue(20, portMAX_DELAY, 5);
35+
AudioPlayerThreadSafe<QueueRTOS> player_save(player, queue);
36+
Task dlna_task("dlna", 8000, 10, 0);
37+
3338
void onEOF(AudioPlayer& player) {
3439
if (source.size() > 0) {
3540
Serial.println("*** onEOF() ***");
36-
player.end();
41+
player_save.end();
3742
source.clear();
3843
media_renderer.setPlaybackCompleted();
3944
}
@@ -45,32 +50,32 @@ void handleMediaEvent(MediaEvent ev, DLNAMediaRenderer<WiFiClient>& mr) {
4550
Serial.print("Event: SET_URI ");
4651
Serial.println(mr.getCurrentUri());
4752
source.clear();
48-
source.addURL(mr.getCurrentUri());
4953
source.setTimeoutAutoNext(1000);
50-
player.begin(0, false);
54+
player_save.setPath(mr.getCurrentUri());
55+
player_save.begin(0, false);
5156
break;
5257
case MediaEvent::PLAY:
5358
Serial.println("Event: PLAY");
54-
player.setActive(true);
59+
player_save.setActive(true);
5560
break;
5661
case MediaEvent::STOP:
5762
Serial.println("Event: STOP");
58-
player.end();
63+
player_save.end();
5964
url.end();
6065
break;
6166
case MediaEvent::PAUSE:
6267
Serial.println("Event: PAUSE");
63-
player.setActive(false);
68+
player_save.setActive(false);
6469
break;
6570
case MediaEvent::SET_VOLUME:
6671
Serial.print("Event: SET_VOLUME ");
6772
Serial.println(mr.getVolume());
68-
player.setVolume(static_cast<float>(mr.getVolume()) / 100.0);
73+
player_save.setVolume(static_cast<float>(mr.getVolume()) / 100.0);
6974
break;
7075
case MediaEvent::SET_MUTE:
7176
Serial.print("Event: SET_MUTE ");
7277
Serial.println(mr.isMuted() ? 1 : 0);
73-
player.setMuted(mr.isMuted());
78+
player_save.setMuted(mr.isMuted());
7479
break;
7580
default:
7681
Serial.println("Event: OTHER");
@@ -125,5 +130,6 @@ void setup() {
125130
}
126131

127132
void loop() {
128-
if (player) player.copy();
133+
// if we have nothing to copy, be nice to other tasks
134+
if (player_save.copy()==0) delay(200);
129135
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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

src/AudioTools/Concurrency/LockGuard.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ class LockGuard {
3434
MutexBase *p_mutex = nullptr;
3535
};
3636

37+
3738
} // namespace audio_tools

src/AudioTools/Concurrency/Mutex.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ class StdMutex : public MutexBase {
6969
std::mutex std_mutex;
7070
};
7171

72+
/**
73+
* @brief Mutex implemntation based on std::mutex
74+
* @ingroup concurrency
75+
* @author Phil Schatzmann
76+
* @copyright GPLv3
77+
*/
78+
class StdRecursiveMutex : public MutexBase {
79+
public:
80+
void lock() override { std_mutex.lock(); }
81+
void unlock() override { std_mutex.unlock(); }
82+
83+
protected:
84+
std::recursive_mutex std_mutex;
85+
};
86+
87+
7288
#endif
7389

7490
} // namespace audio_tools

src/AudioTools/Concurrency/RTOS/MutexRTOS.h

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class MutexRTOS : public MutexBase {
2323
public:
2424
MutexRTOS() {
2525
xSemaphore = xSemaphoreCreateBinary();
26-
xSemaphoreGive(xSemaphore);
26+
unlock();
2727
}
2828
virtual ~MutexRTOS() {
2929
vSemaphoreDelete(xSemaphore);
@@ -39,6 +39,34 @@ class MutexRTOS : public MutexBase {
3939
SemaphoreHandle_t xSemaphore = NULL;
4040
};
4141

42+
/**
43+
* @brief Recursive Mutex implemntation using FreeRTOS
44+
* @ingroup concurrency
45+
* @author Phil Schatzmann
46+
* @copyright GPLv3 *
47+
*/
48+
49+
class MutexRecursiveRTOS : public MutexBase {
50+
public:
51+
MutexRecursiveRTOS() {
52+
xSemaphore = xSemaphoreCreateBinary();
53+
unlock();
54+
}
55+
virtual ~MutexRecursiveRTOS() {
56+
vSemaphoreDelete(xSemaphore);
57+
}
58+
void lock() override {
59+
xSemaphoreTakeRecursive(xSemaphore, portMAX_DELAY);
60+
}
61+
void unlock() override {
62+
xSemaphoreGiveRecursive(xSemaphore);
63+
}
64+
65+
protected:
66+
SemaphoreHandle_t xSemaphore = NULL;
67+
};
68+
69+
4270
/// @brief Default Mutex implementation using RTOS semaphores
4371
/// @ingroup concurrency
4472
using Mutex = MutexRTOS;

src/AudioTools/CoreAudio/AudioPlayer.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ class AudioPlayer : public AudioInfoSupport, public VolumeSupport {
535535
}
536536

537537
/// Mutes or unmutes the audio player
538-
void setMuted(bool muted) {
538+
bool setMuted(bool muted) {
539539
if (muted) {
540540
if (current_volume > 0.0f) {
541541
muted_volume = current_volume;
@@ -547,6 +547,7 @@ class AudioPlayer : public AudioInfoSupport, public VolumeSupport {
547547
muted_volume = 0.0f;
548548
}
549549
}
550+
return true;
550551
}
551552

552553
/// Returns true if the player is currently muted

0 commit comments

Comments
 (0)