arduino-audio-tools
All Classes Namespaces Files Functions Variables Typedefs Enumerations Friends Modules Pages
A2DPStream.h
Go to the documentation of this file.
1
8#pragma once
9
10#include "AudioConfig.h"
11
12#include "AudioTools.h"
13#include "BluetoothA2DPSink.h"
14#include "BluetoothA2DPSource.h"
15#include "AudioTools/CoreAudio/AudioStreams.h"
16#include "AudioTools/Concurrency/RTOS/BufferRTOS.h"
17#include "AudioTools/CoreAudio/AudioBasic/StrView.h"
18
19
20namespace audio_tools {
21
22class A2DPStream;
23static A2DPStream *A2DPStream_self=nullptr;
24// buffer which is used to exchange data
25static BufferRTOS<uint8_t>a2dp_buffer{0, A2DP_BUFFER_SIZE, portMAX_DELAY, portMAX_DELAY};
26// flag to indicated that we are ready to process data
27static bool is_a2dp_active = false;
28
29int32_t a2dp_stream_source_sound_data(Frame* data, int32_t len);
30void a2dp_stream_sink_sound_data(const uint8_t* data, uint32_t len);
31
33enum A2DPStartLogic {StartWhenBufferFull, StartOnConnect};
35enum A2DPNoData {A2DPSilence, A2DPWhoosh};
36
43 public:
45 A2DPStartLogic startup_logic = StartWhenBufferFull;
49 RxTxMode mode = RX_MODE;
51 const char* name = "A2DP";
53 bool auto_reconnect = false;
54 int buffer_size = A2DP_BUFFER_SIZE * A2DP_BUFFER_COUNT;
56 int delay_ms = 1;
58 bool silence_on_nodata = false;
60 int tx_write_timeout_ms = -1; // no timeout
63};
64
65
83class A2DPStream : public AudioStream, public VolumeSupport {
84
85 public:
86 A2DPStream() {
87 TRACED();
88 // A2DPStream can only be used once
89 assert(A2DPStream_self==nullptr);
90 A2DPStream_self = this;
91 info.bits_per_sample = 16;
92 info.sample_rate = 44100;
93 info.channels = 2;
94 }
95
98 TRACED();
99 if (a2dp_source!=nullptr) delete a2dp_source;
100 if (a2dp_sink!=nullptr) delete a2dp_sink;
101 A2DPStream_self = nullptr;
102 }
103
104 A2DPConfig defaultConfig(RxTxMode mode=RX_MODE){
105 A2DPConfig cfg;
106 cfg.mode = mode;
107 if(mode==TX_MODE){
108 cfg.name="[Unknown]";
109 }
110 return cfg;
111 }
112
115 if (a2dp_source==nullptr){
116 a2dp = a2dp_source = new BluetoothA2DPSource();
117 }
118 return *a2dp_source;
119 }
120
123 if (a2dp_sink==nullptr){
124 a2dp = a2dp_sink = new BluetoothA2DPSink();
125 }
126 return *a2dp_sink;
127 }
128
130 bool begin(RxTxMode mode, const char* name, bool wait_for_connection=true){
131 A2DPConfig cfg;
132 cfg.mode = mode;
133 cfg.name = name;
134 cfg.wait_for_connection = wait_for_connection;
135 return begin(cfg);
136 }
137
139 bool begin(A2DPConfig cfg){
140 this->config = cfg;
141 bool result = false;
142 LOGI("Connecting to %s",cfg.name);
143
144 if (!a2dp_buffer.resize(cfg.buffer_size)){
145 LOGE("a2dp_buffer resize failed");
146 return false;
147 }
148
149 // initialize a2dp_silence_timeout
150 if (config.silence_on_nodata){
151 LOGI("Using StartOnConnect")
152 config.startup_logic = StartOnConnect;
153 }
154
155 switch (cfg.mode){
156 case TX_MODE:
157 LOGI("Starting a2dp_source...");
158 source(); // allocate object
159 a2dp_source->set_auto_reconnect(cfg.auto_reconnect);
160 a2dp_source->set_volume(volume() * A2DP_MAX_VOL);
161 if(StrView(cfg.name).equals("[Unknown]")){
162 //search next available device
163 a2dp_source->set_ssid_callback(detected_device);
164 }
165 a2dp_source->set_on_connection_state_changed(a2dp_state_callback, this);
166 a2dp_source->start_raw((char*)cfg.name, a2dp_stream_source_sound_data);
167 if (cfg.wait_for_connection){
168 while(!a2dp_source->is_connected()){
169 LOGD("waiting for connection");
170 delay(1000);
171 }
172 LOGI("a2dp_source is connected...");
173 notify_base_Info(44100);
174 //is_a2dp_active = true;
175 }
176 else{
177 LOGI("a2dp_source started without connecting");
178 }
179 result = true;
180 break;
181
182 case RX_MODE:
183 LOGI("Starting a2dp_sink...");
184 sink(); // allocate object
185 a2dp_sink->set_auto_reconnect(cfg.auto_reconnect);
186 a2dp_sink->set_stream_reader(&a2dp_stream_sink_sound_data, false);
187 a2dp_sink->set_volume(volume() * A2DP_MAX_VOL);
188 a2dp_sink->set_on_connection_state_changed(a2dp_state_callback, this);
189 a2dp_sink->set_sample_rate_callback(sample_rate_callback);
190 a2dp_sink->start((char*)cfg.name);
191 if (cfg.wait_for_connection){
192 while(!a2dp_sink->is_connected()){
193 LOGD("waiting for connection");
194 delay(1000);
195 }
196 LOGI("a2dp_sink is connected...");
197 }
198 else{
199 LOGI("a2dp_sink started without connection");
200 }
201 is_a2dp_active = true;
202 result = true;
203 break;
204 default:
205 LOGE("Undefined mode: %d", cfg.mode);
206 break;
207 }
208
209 return result;
210 }
211
212 void end() override {
213 if (a2dp != nullptr) {
214 a2dp->disconnect();
215 }
216 AudioStream::end();
217 }
218
220 bool isConnected() {
221 if (a2dp_source==nullptr && a2dp_sink==nullptr) return false;
222 if (a2dp_source!=nullptr) return a2dp_source->is_connected();
223 return a2dp_sink->is_connected();
224 }
225
227 bool isReady() {
228 return is_a2dp_active;
229 }
230
232 operator bool() {
233 return isReady();
234 }
235
237 size_t write(const uint8_t* data, size_t len) override {
238 LOGD("%s: %zu", LOG_METHOD, len);
239 if (config.mode == TX_MODE){
240 // at 80% we activate the processing
241 if(!is_a2dp_active
242 && config.startup_logic == StartWhenBufferFull
243 && a2dp_buffer.available() >= 0.8f * a2dp_buffer.size()){
244 LOGI("set active");
245 is_a2dp_active = true;
246 }
247
248 // blocking write: if buffer is full we wait
249 int timeout = config.tx_write_timeout_ms;
250 int wait_time = 5;
251 size_t free = a2dp_buffer.availableForWrite();
252 while(len > free){
253 LOGD("Waiting for buffer: writing %d > available %d", (int) len, (int) free);
254 if (timeout > 0) {
255 timeout -= wait_time;
256 if (timeout <= 0) return 0;
257 }
258 delay(wait_time);
259 free = a2dp_buffer.availableForWrite();
260 }
261 }
262
263 // write to buffer
264 size_t result = a2dp_buffer.writeArray(data, len);
265 LOGD("write %d -> %d", len, result);
266 if (config.mode == TX_MODE){
267 // give the callback a chance to retrieve the data
268 delay(config.delay_ms);
269 }
270 return result;
271 }
272
274 size_t readBytes(uint8_t *data, size_t len) override {
275 if (!is_a2dp_active){
276 LOGW( "readBytes failed because !is_a2dp_active");
277 return 0;
278 }
279 LOGD("readBytes %d", len);
280 size_t result = a2dp_buffer.readArray(data, len);
281 LOGI("readBytes %d->%d", len,result);
282 return result;
283 }
284
286 int available() override {
287 // only supported in tx mode
288 if (config.mode!=RX_MODE) return 0;
289 return a2dp_buffer.available();
290 }
291
293 int availableForWrite() override {
294 // only supported in tx mode
295 if (config.mode!=TX_MODE ) return 0;
296 // return infor from buffer
297 return a2dp_buffer.availableForWrite();
298 }
299
301 bool setVolume(float volume) override {
303 // 128 is max volume
304 if (a2dp!=nullptr) a2dp->set_volume(volume * A2DP_MAX_VOL);
305 return true;
306 }
307
310 return a2dp_buffer;
311 }
312
314 void setSilenceOnNoData(bool silence){
315 config.silence_on_nodata = silence;
316 }
317
318 protected:
319 A2DPConfig config;
320 BluetoothA2DPSource *a2dp_source = nullptr;
321 BluetoothA2DPSink *a2dp_sink = nullptr;
322 BluetoothA2DPCommon *a2dp=nullptr;
323 const int A2DP_MAX_VOL = 128;
324
325 // auto-detect device to send audio to (TX-Mode)
326 static bool detected_device(const char* ssid, esp_bd_addr_t address, int rssi){
327 LOGW("found Device: %s rssi: %d", ssid, rssi);
328 //filter out weak signals
329 return (rssi > -75);
330 }
331
332 static void a2dp_state_callback(esp_a2d_connection_state_t state, void *caller){
333 TRACED();
334 A2DPStream *self = (A2DPStream*)caller;
335 if (state==ESP_A2D_CONNECTION_STATE_CONNECTED && self->config.startup_logic==StartOnConnect){
336 is_a2dp_active = true;
337 }
338 LOGW("==> state: %s", self->a2dp->to_str(state));
339 }
340
341
342 // callback used by A2DP to provide the a2dp_source sound data
343 static int32_t a2dp_stream_source_sound_data(uint8_t* data, int32_t len) {
344 int32_t result_len = 0;
345 A2DPConfig config = A2DPStream_self->config;
346
347 // at first call we start with some empty data
348 if (is_a2dp_active){
349 // the data in the file must be in int16 with 2 channels
350 yield();
351 result_len = a2dp_buffer.readArray((uint8_t*)data, len);
352
353 // provide silence data
354 if (config.silence_on_nodata && result_len == 0){
355 memset(data,0, len);
356 result_len = len;
357 }
358 } else {
359
360 // prevent underflow on first call
361 switch (config.startup_nodata) {
362 case A2DPSilence:
363 memset(data, 0, len);
364 break;
365 case A2DPWhoosh:
366 int16_t *data16 = (int16_t*)data;
367 for (int j=0;j<len/4;j+=2){
368 data16[j+1] = data16[j] = (rand() % 50) - 25;
369 }
370 break;
371 }
372 result_len = len;
373
374 // Priority: 22 on core 0
375 // LOGI("Priority: %d on core %d", uxTaskPriorityGet(NULL), xPortGetCoreID());
376
377 }
378 LOGD("a2dp_stream_source_sound_data: %d -> %d", len, result_len);
379 return result_len;
380 }
381
383 static void a2dp_stream_sink_sound_data(const uint8_t* data, uint32_t len) {
384 if (is_a2dp_active){
385 uint32_t result_len = a2dp_buffer.writeArray(data, len);
386 LOGD("a2dp_stream_sink_sound_data %d -> %d", len, result_len);
387 }
388 }
389
391 void notify_base_Info(int rate){
392 AudioInfo info;
393 info.channels = 2;
394 info.bits_per_sample = 16;
395 info.sample_rate = rate;
396 notifyAudioChange(info);
397 }
398
400 static void sample_rate_callback(uint16_t rate) {
401 A2DPStream_self->info.sample_rate = rate;
402 A2DPStream_self->notify_base_Info(rate);
403 }
404
405};
406
407}
Configuration for A2DPStream.
Definition A2DPStream.h:42
bool silence_on_nodata
when a2dp source is active but has no data we generate silence data (default false)
Definition A2DPStream.h:58
bool auto_reconnect
automatically reconnect if connection is lost (default false)
Definition A2DPStream.h:53
int delay_ms
Delay in ms which is added to each write (default 1)
Definition A2DPStream.h:56
A2DPNoData startup_nodata
Action when a2dp is not active yet (default A2DPSilence)
Definition A2DPStream.h:47
int tx_write_timeout_ms
write timeout in ms: -1 is blocking write (default -1)
Definition A2DPStream.h:60
const char * name
A2DP name (default A2DP)
Definition A2DPStream.h:51
A2DPStartLogic startup_logic
Logic when the processing is activated (default StartWhenBufferFull)
Definition A2DPStream.h:45
bool wait_for_connection
begin should wait for connection to be established (default true)
Definition A2DPStream.h:62
RxTxMode mode
Mode: TX_MODE or RX_MODE (default RX_MODE)
Definition A2DPStream.h:49
Stream support for A2DP using https://github.com/pschatzmann/ESP32-A2DP: begin(TX_MODE) opens a a2dp_...
Definition A2DPStream.h:83
BluetoothA2DPSource & source()
provides access to the
Definition A2DPStream.h:114
void notify_base_Info(int rate)
notify subscriber with AudioInfo
Definition A2DPStream.h:391
size_t readBytes(uint8_t *data, size_t len) override
Reads the data from the temporary buffer.
Definition A2DPStream.h:274
static void a2dp_stream_sink_sound_data(const uint8_t *data, uint32_t len)
callback used by A2DP to write the sound data
Definition A2DPStream.h:383
bool setVolume(float volume) override
Define the volume (values between 0.0 and 1.0)
Definition A2DPStream.h:301
int available() override
Provides the number of bytes available to read.
Definition A2DPStream.h:286
static void sample_rate_callback(uint16_t rate)
callback to update audio info with used a2dp sample rate
Definition A2DPStream.h:400
size_t write(const uint8_t *data, size_t len) override
Writes the data into a temporary send buffer - where it can be picked up by the callback.
Definition A2DPStream.h:237
bool isConnected()
checks if we are connected
Definition A2DPStream.h:220
void setSilenceOnNoData(bool silence)
Manage config.silence_on_nodata dynamically.
Definition A2DPStream.h:314
int availableForWrite() override
Provides the number of bytes available to write.
Definition A2DPStream.h:293
BaseBuffer< uint8_t > & buffer()
Provides access to the buffer.
Definition A2DPStream.h:309
BluetoothA2DPSink & sink()
provides access to the BluetoothA2DPSink
Definition A2DPStream.h:122
~A2DPStream()
Release the allocate a2dp_source or a2dp_sink.
Definition A2DPStream.h:97
bool isReady()
is ready to process data
Definition A2DPStream.h:227
bool begin(RxTxMode mode, const char *name, bool wait_for_connection=true)
Starts the processing.
Definition A2DPStream.h:130
bool begin(A2DPConfig cfg)
Starts the processing.
Definition A2DPStream.h:139
Base class for all Audio Streams. It support the boolean operator to test if the object is ready with...
Definition BaseStream.h:115
A simple wrapper to provide string functions on existing allocated char*. If the underlying char* is ...
Definition StrView.h:28
virtual bool equals(const char *str)
checks if the string equals indicated parameter string
Definition StrView.h:165
Vector implementation which provides the most important methods as defined by std::vector....
Definition Vector.h:21
Supports the setting and getting of the volume.
Definition AudioTypes.h:201
virtual float volume()
provides the actual volume in the range of 0.0f to 1.0f
Definition AudioTypes.h:204
virtual bool setVolume(float volume)
define the actual volume in the range of 0.0f to 1.0f
Definition AudioTypes.h:206
RxTxMode
The Microcontroller is the Audio Source (TX_MODE) or Audio Sink (RX_MODE). RXTX_MODE is Source and Si...
Definition AudioTypes.h:28
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition AudioConfig.h:885
A2DPNoData
A2DP Action when there is no data.
Definition A2DPStream.h:35
A2DPStartLogic
A2DP Startup Logic.
Definition A2DPStream.h:33
Basic Audio information which drives e.g. I2S.
Definition AudioTypes.h:52
sample_rate_t sample_rate
Sample Rate: e.g 44100.
Definition AudioTypes.h:55
uint16_t channels
Number of channels: 2=stereo, 1=mono.
Definition AudioTypes.h:57
uint8_t bits_per_sample
Number of bits per sample (int16_t = 16 bits)
Definition AudioTypes.h:59