arduino-audio-tools
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/BufferRTOS.h"
17 #include "AudioTools/CoreAudio/AudioBasic/StrView.h"
18 
19 
20 namespace audio_tools {
21 
22 class A2DPStream;
23 static A2DPStream *A2DPStream_self=nullptr;
24 // buffer which is used to exchange data
25 static BufferRTOS<uint8_t>a2dp_buffer{0, A2DP_BUFFER_SIZE, portMAX_DELAY, portMAX_DELAY};
26 // flag to indicated that we are ready to process data
27 static bool is_a2dp_active = false;
28 
29 int32_t a2dp_stream_source_sound_data(Frame* data, int32_t len);
30 void a2dp_stream_sink_sound_data(const uint8_t* data, uint32_t len);
31 
33 enum A2DPStartLogic {StartWhenBufferFull, StartOnConnect};
35 enum A2DPNoData {A2DPSilence, A2DPWhoosh};
36 
42 class A2DPConfig {
43  public:
45  A2DPStartLogic startup_logic = StartWhenBufferFull;
47  A2DPNoData startup_nodata = A2DPSilence;
48  RxTxMode mode = RX_MODE;
50  const char* name = "A2DP";
51  bool auto_reconnect = false;
52  int buffer_size = A2DP_BUFFER_SIZE * A2DP_BUFFER_COUNT;
54  int delay_ms = 1;
56  bool silence_on_nodata = false;
58  int tx_write_timeout_ms = -1; // no timeout
59 };
60 
61 
79 class A2DPStream : public AudioStream, public VolumeSupport {
80 
81  public:
82  A2DPStream() {
83  TRACED();
84  // A2DPStream can only be used once
85  assert(A2DPStream_self==nullptr);
86  A2DPStream_self = this;
87  info.bits_per_sample = 16;
88  info.sample_rate = 44100;
89  info.channels = 2;
90  }
91 
94  TRACED();
95  if (a2dp_source!=nullptr) delete a2dp_source;
96  if (a2dp_sink!=nullptr) delete a2dp_sink;
97  A2DPStream_self = nullptr;
98  }
99 
100  A2DPConfig defaultConfig(RxTxMode mode=RX_MODE){
101  A2DPConfig cfg;
102  cfg.mode = mode;
103  if(mode==TX_MODE){
104  cfg.name="[Unknown]";
105  }
106  return cfg;
107  }
108 
110  BluetoothA2DPSource &source() {
111  if (a2dp_source==nullptr){
112  a2dp = a2dp_source = new BluetoothA2DPSource();
113  }
114  return *a2dp_source;
115  }
116 
118  BluetoothA2DPSink &sink(){
119  if (a2dp_sink==nullptr){
120  a2dp = a2dp_sink = new BluetoothA2DPSink();
121  }
122  return *a2dp_sink;
123  }
124 
126  bool begin(RxTxMode mode, const char* name){
127  A2DPConfig cfg;
128  cfg.mode = mode;
129  cfg.name = name;
130  return begin(cfg);
131  }
132 
134  bool begin(A2DPConfig cfg){
135  this->config = cfg;
136  bool result = false;
137  LOGI("Connecting to %s",cfg.name);
138 
139  if (!a2dp_buffer.resize(cfg.buffer_size)){
140  LOGE("a2dp_buffer resize failed");
141  return false;
142  }
143 
144  // initialize a2dp_silence_timeout
145  if (config.silence_on_nodata){
146  LOGI("Using StartOnConnect")
147  config.startup_logic = StartOnConnect;
148  }
149 
150  switch (cfg.mode){
151  case TX_MODE:
152  LOGI("Starting a2dp_source...");
153  source(); // allocate object
154  a2dp_source->set_auto_reconnect(cfg.auto_reconnect);
155  a2dp_source->set_volume(volume() * A2DP_MAX_VOL);
156  if(StrView(cfg.name).equals("[Unknown]")){
157  //search next available device
158  a2dp_source->set_ssid_callback(detected_device);
159  }
160  a2dp_source->set_on_connection_state_changed(a2dp_state_callback, this);
161  a2dp_source->start_raw((char*)cfg.name, a2dp_stream_source_sound_data);
162  while(!a2dp_source->is_connected()){
163  LOGD("waiting for connection");
164  delay(1000);
165  }
166  LOGI("a2dp_source is connected...");
167  notify_base_Info(44100);
168  //is_a2dp_active = true;
169  result = true;
170  break;
171 
172  case RX_MODE:
173  LOGI("Starting a2dp_sink...");
174  sink(); // allocate object
175  a2dp_sink->set_auto_reconnect(cfg.auto_reconnect);
176  a2dp_sink->set_stream_reader(&a2dp_stream_sink_sound_data, false);
177  a2dp_sink->set_volume(volume() * A2DP_MAX_VOL);
178  a2dp_sink->set_on_connection_state_changed(a2dp_state_callback, this);
179  a2dp_sink->set_sample_rate_callback(sample_rate_callback);
180  a2dp_sink->start((char*)cfg.name);
181  while(!a2dp_sink->is_connected()){
182  LOGD("waiting for connection");
183  delay(1000);
184  }
185  LOGI("a2dp_sink is connected...");
186  is_a2dp_active = true;
187  result = true;
188  break;
189  default:
190  LOGE("Undefined mode: %d", cfg.mode);
191  break;
192  }
193 
194  return result;
195  }
196 
197  void end() override {
198  if (a2dp != nullptr) {
199  a2dp->disconnect();
200  }
201  AudioStream::end();
202  }
203 
205  bool isConnected() {
206  if (a2dp_source==nullptr && a2dp_sink==nullptr) return false;
207  if (a2dp_source!=nullptr) return a2dp_source->is_connected();
208  return a2dp_sink->is_connected();
209  }
210 
212  bool isReady() {
213  return is_a2dp_active;
214  }
215 
217  operator bool() {
218  return isReady();
219  }
220 
222  size_t write(const uint8_t* data, size_t len) override {
223  LOGD("%s: %zu", LOG_METHOD, len);
224  if (config.mode == TX_MODE){
225  // at 80% we activate the processing
226  if(!is_a2dp_active
227  && config.startup_logic == StartWhenBufferFull
228  && a2dp_buffer.available() >= 0.8f * a2dp_buffer.size()){
229  LOGI("set active");
230  is_a2dp_active = true;
231  }
232 
233  // blocking write: if buffer is full we wait
234  int timeout = config.tx_write_timeout_ms;
235  int wait_time = 5;
236  size_t free = a2dp_buffer.availableForWrite();
237  while(len > free){
238  LOGD("Waiting for buffer: writing %d > available %d", (int) len, (int) free);
239  if (timeout > 0) {
240  timeout -= wait_time;
241  if (timeout <= 0) return 0;
242  }
243  delay(wait_time);
244  free = a2dp_buffer.availableForWrite();
245  }
246  }
247 
248  // write to buffer
249  size_t result = a2dp_buffer.writeArray(data, len);
250  LOGD("write %d -> %d", len, result);
251  if (config.mode == TX_MODE){
252  // give the callback a chance to retrieve the data
253  delay(config.delay_ms);
254  }
255  return result;
256  }
257 
259  size_t readBytes(uint8_t *data, size_t len) override {
260  if (!is_a2dp_active){
261  LOGW( "readBytes failed because !is_a2dp_active");
262  return 0;
263  }
264  LOGD("readBytes %d", len);
265  size_t result = a2dp_buffer.readArray(data, len);
266  LOGI("readBytes %d->%d", len,result);
267  return result;
268  }
269 
271  int available() override {
272  // only supported in tx mode
273  if (config.mode!=RX_MODE) return 0;
274  return a2dp_buffer.available();
275  }
276 
278  int availableForWrite() override {
279  // only supported in tx mode
280  if (config.mode!=TX_MODE ) return 0;
281  // return infor from buffer
282  return a2dp_buffer.availableForWrite();
283  }
284 
286  bool setVolume(float volume) override {
288  // 128 is max volume
289  if (a2dp!=nullptr) a2dp->set_volume(volume * A2DP_MAX_VOL);
290  return true;
291  }
292 
295  return a2dp_buffer;
296  }
297 
299  void setSilenceOnNoData(bool silence){
300  config.silence_on_nodata = silence;
301  }
302 
303  protected:
304  A2DPConfig config;
305  BluetoothA2DPSource *a2dp_source = nullptr;
306  BluetoothA2DPSink *a2dp_sink = nullptr;
307  BluetoothA2DPCommon *a2dp=nullptr;
308  const int A2DP_MAX_VOL = 128;
309 
310  // auto-detect device to send audio to (TX-Mode)
311  static bool detected_device(const char* ssid, esp_bd_addr_t address, int rssi){
312  LOGW("found Device: %s rssi: %d", ssid, rssi);
313  //filter out weak signals
314  return (rssi > -75);
315  }
316 
317  static void a2dp_state_callback(esp_a2d_connection_state_t state, void *caller){
318  TRACED();
319  A2DPStream *self = (A2DPStream*)caller;
320  if (state==ESP_A2D_CONNECTION_STATE_CONNECTED && self->config.startup_logic==StartOnConnect){
321  is_a2dp_active = true;
322  }
323  LOGW("==> state: %s", self->a2dp->to_str(state));
324  }
325 
326 
327  // callback used by A2DP to provide the a2dp_source sound data
328  static int32_t a2dp_stream_source_sound_data(uint8_t* data, int32_t len) {
329  int32_t result_len = 0;
330  A2DPConfig config = A2DPStream_self->config;
331 
332  // at first call we start with some empty data
333  if (is_a2dp_active){
334  // the data in the file must be in int16 with 2 channels
335  yield();
336  result_len = a2dp_buffer.readArray((uint8_t*)data, len);
337 
338  // provide silence data
339  if (config.silence_on_nodata && result_len == 0){
340  memset(data,0, len);
341  result_len = len;
342  }
343  } else {
344 
345  // prevent underflow on first call
346  switch (config.startup_nodata) {
347  case A2DPSilence:
348  memset(data, 0, len);
349  break;
350  case A2DPWhoosh:
351  int16_t *data16 = (int16_t*)data;
352  for (int j=0;j<len/4;j+=2){
353  data16[j+1] = data16[j] = (rand() % 50) - 25;
354  }
355  break;
356  }
357  result_len = len;
358 
359  // Priority: 22 on core 0
360  // LOGI("Priority: %d on core %d", uxTaskPriorityGet(NULL), xPortGetCoreID());
361 
362  }
363  LOGD("a2dp_stream_source_sound_data: %d -> %d", len, result_len);
364  return result_len;
365  }
366 
368  static void a2dp_stream_sink_sound_data(const uint8_t* data, uint32_t len) {
369  if (is_a2dp_active){
370  uint32_t result_len = a2dp_buffer.writeArray(data, len);
371  LOGD("a2dp_stream_sink_sound_data %d -> %d", len, result_len);
372  }
373  }
374 
376  void notify_base_Info(int rate){
377  AudioInfo info;
378  info.channels = 2;
379  info.bits_per_sample = 16;
380  info.sample_rate = rate;
381  notifyAudioChange(info);
382  }
383 
385  static void sample_rate_callback(uint16_t rate) {
386  A2DPStream_self->info.sample_rate = rate;
387  A2DPStream_self->notify_base_Info(rate);
388  }
389 
390 };
391 
392 }
Configuration for A2DPStream.
Definition: A2DPStream.h:42
bool silence_on_nodata
when a2dp source is active but has no data we generate silence data
Definition: A2DPStream.h:56
int delay_ms
Delay in ms which is added to each write.
Definition: A2DPStream.h:54
A2DPNoData startup_nodata
Action when a2dp is not active yet.
Definition: A2DPStream.h:47
int tx_write_timeout_ms
write timeout in ms: -1 is blocking write
Definition: A2DPStream.h:58
const char * name
A2DP name.
Definition: A2DPStream.h:50
A2DPStartLogic startup_logic
Logic when the processing is activated.
Definition: A2DPStream.h:45
Stream support for A2DP using https://github.com/pschatzmann/ESP32-A2DP: begin(TX_MODE) opens a a2dp_...
Definition: A2DPStream.h:79
void notify_base_Info(int rate)
notify subscriber with AudioInfo
Definition: A2DPStream.h:376
BluetoothA2DPSink & sink()
provides access to the BluetoothA2DPSink
Definition: A2DPStream.h:118
BaseBuffer< uint8_t > & buffer()
Provides access to the buffer.
Definition: A2DPStream.h:294
size_t readBytes(uint8_t *data, size_t len) override
Reads the data from the temporary buffer.
Definition: A2DPStream.h:259
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:368
bool setVolume(float volume) override
Define the volume (values between 0.0 and 1.0)
Definition: A2DPStream.h:286
int available() override
Provides the number of bytes available to read.
Definition: A2DPStream.h:271
static void sample_rate_callback(uint16_t rate)
callback to update audio info with used a2dp sample rate
Definition: A2DPStream.h:385
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:222
bool isConnected()
checks if we are connected
Definition: A2DPStream.h:205
void setSilenceOnNoData(bool silence)
Manage config.silence_on_nodata dynamically.
Definition: A2DPStream.h:299
int availableForWrite() override
Provides the number of bytes available to write.
Definition: A2DPStream.h:278
bool begin(RxTxMode mode, const char *name)
Starts the processing.
Definition: A2DPStream.h:126
~A2DPStream()
Release the allocate a2dp_source or a2dp_sink.
Definition: A2DPStream.h:93
bool isReady()
is ready to process data
Definition: A2DPStream.h:212
BluetoothA2DPSource & source()
provides access to the
Definition: A2DPStream.h:110
bool begin(A2DPConfig cfg)
Starts the processing.
Definition: A2DPStream.h:134
Base class for all Audio Streams. It support the boolean operator to test if the object is ready with...
Definition: BaseStream.h:109
int available() override
provides the number of entries that are available to read
Definition: BufferRTOS.h:137
int availableForWrite() override
provides the number of entries that are available to write
Definition: BufferRTOS.h:142
int writeArray(const T data[], int len)
Fills the buffer data.
Definition: BufferRTOS.h:95
bool resize(size_t size)
Re-Allocats the memory and the queue.
Definition: BufferRTOS.h:50
int readArray(T data[], int len)
reads multiple values
Definition: BufferRTOS.h:77
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
Supports the setting and getting of the volume.
Definition: AudioTypes.h:207
virtual float volume()
provides the actual volume in the range of 0.0f to 1.0f
Definition: AudioTypes.h:210
virtual bool setVolume(float volume)
define the actual volume in the range of 0.0f to 1.0f
Definition: AudioTypes.h:212
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:868
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