A2DPStream Corrections

This commit is contained in:
pschatzmann 2023-03-22 12:52:45 +01:00
parent b201028a79
commit 9598c95ec7
5 changed files with 167 additions and 95 deletions

View File

@ -0,0 +1,41 @@
/**
* @file streams-a2dp-serial.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/examples-stream/streams-a2dp-serial/README.md
*
* @author Phil Schatzmann
* @copyright GPLv3
*
*/
#include "AudioTools.h"
#include "AudioLibs/AudioA2DP.h"
#include "AudioLibs/AudioKit.h"
A2DPStream in;
AudioKitStream kit;
StreamCopy copier(kit, in); // copy in to out
// Arduino Setup
void setup(void) {
Serial.begin(115200);
AudioLogger::instance().begin(Serial, AudioLogger::Warning);
// start the bluetooth audio receiver
Serial.println("starting A2DP...");
auto cfg = in.defaultConfig(RX_MODE);
cfg.name = "AudioKit";
in.begin(cfg);
// setup the audioKit
auto cfgk = kit.defaultConfig(TX_MODE);
cfgk.copyFrom(in.audioInfo());
kit.begin(cfgk);
}
// Arduino loop
void loop() {
copier.copy();
}

View File

@ -0,0 +1,49 @@
/**
* @file base-generator-a2dp.ino
* @author Phil Schatzmann
* @brief We send a test sine signal to a bluetooth speaker
* @copyright GPLv3
*/
/**
* @file base-generator-a2dp.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/base-generator-a2dp/README.md
* @copyright GPLv3
*/
#include "AudioTools.h"
#include "AudioLibs/AudioA2DP.h"
const char* name = "LEXON MINO L"; // Replace with your bluetooth speaker name
SineWaveGenerator<int16_t> sineWave(15000); // subclass of SoundGenerator, set max amplitude (=volume)
GeneratedSoundStream<int16_t> in_stream(sineWave); // Stream generated from sine wave
BluetoothA2DPSource a2dp_source; // A2DP Sender
// callback used by A2DP to provide the sound data - usually len is 128 * 2 channel int16 frames
int32_t get_sound_data(uint8_t * data, int32_t len) {
return in_stream.readBytes((uint8_t*)data, len);
}
// Arduino Setup
void setup(void) {
Serial.begin(115200);
AudioLogger::instance().begin(Serial, AudioLogger::Warning);
// start input
auto cfg = in_stream.defaultConfig();
cfg.bits_per_sample = 16;
cfg.channels = 2;
cfg.sample_rate = 44100;
in_stream.begin(cfg);
sineWave.begin(cfg, N_B4);
// start the bluetooth
Serial.println("starting A2DP...");
a2dp_source.set_auto_reconnect(false);
a2dp_source.start_raw(name, get_sound_data);
}
// Arduino loop - repeated processing
void loop() {
}

View File

@ -11,11 +11,11 @@
#include "AudioTools.h"
#include "AudioLibs/AudioA2DP.h"
typedef int16_t sound_t; // sound will be represented as int16_t (with 2 bytes)
const char* name = "LEXON MINO L"; // Replace with your device name
uint16_t sample_rate=44100;
uint8_t channels = 2; // The stream will have 2 channels
SineWaveGenerator<sound_t> sineWave(32000); // subclass of SoundGenerator with max amplitude of 32000
GeneratedSoundStream<sound_t> in(sineWave); // Stream generated from sine wave
SineWaveGenerator<int16_t> sineWave(32000); // subclass of SoundGenerator with max amplitude of 32000
GeneratedSoundStream<int16_t> in(sineWave); // Stream generated from sine wave
A2DPStream out; // A2DP output
StreamCopy copier(out, in); // copy in to out
@ -36,7 +36,7 @@ void setup(void) {
// We send the test signal via A2DP - so we conect to the MyMusic Bluetooth Speaker
auto cfgA2DP = out.defaultConfig(TX_MODE);
cfgA2DP.name = "LEXON MINO L";
cfgA2DP.name = name;
//cfgA2DP.auto_reconnect = false;
out.begin(cfgA2DP);
out.setVolume(0.3);

View File

@ -14,6 +14,7 @@
#include "BluetoothA2DPSource.h"
#include "AudioTools/AudioStreams.h"
namespace audio_tools {
/**
@ -45,11 +46,12 @@ class A2DPChannelConverter {
};
class A2DPStream;
static A2DPStream *A2DPStream_self=nullptr;
A2DPStream *A2DPStream_self=nullptr;
// buffer which is used to exchange data
static RingBuffer<uint8_t> *a2dp_buffer = nullptr;
//RingBuffer<uint8_t> a2dp_buffer{0};
SynchronizedBufferRTOS<uint8_t>a2dp_buffer{A2DP_BUFFER_SIZE * A2DP_BUFFER_COUNT, A2DP_BUFFER_SIZE, portMAX_DELAY, portMAX_DELAY};
// flag to indicated that we are ready to process data
static bool is_a2dp_active = false;
bool is_a2dp_active = false;
int32_t a2dp_stream_source_sound_data(Frame* data, int32_t len);
void a2dp_stream_sink_sound_data(const uint8_t* data, uint32_t len);
@ -65,11 +67,12 @@ enum A2DPNoData {A2DPSilence, A2DPWhoosh};
class A2DPConfig {
public:
A2DPStartLogic startLogic = StartWhenBufferFull;
A2DPNoData noData = A2DPSilence;
A2DPNoData noData = A2DPWhoosh;
RxTxMode mode = RX_MODE;
const char* name = "A2DP";
bool auto_reconnect = false;
int bufferSize = A2DP_BUFFER_SIZE * A2DP_BUFFER_COUNT;
int delay_ms = 5;
};
@ -77,6 +80,11 @@ class A2DPConfig {
* @brief Stream support for A2DP: begin(TX_MODE) uses a2dp_source - begin(RX_MODE) a a2dp_sink
* The data is in int16_t with 2 channels at 44100 hertz.
* We support only one instance of the class!
* Please note that this is a conveniance class that supports the stream api,
* however this is rather inefficient, beause quite a bit buffer needs to be allocated.
* It is recommended to use the API with the callbacks. Examples can be found in the examples-basic-api
* directory.
*
* @ingroup io
* @author Phil Schatzmann
* @copyright GPLv3
@ -86,10 +94,12 @@ class A2DPStream : public AudioStream {
public:
A2DPStream() {
TRACED();
xSemaphore = xSemaphoreCreateMutex();
// A2DPStream can only be used once
assert(A2DPStream_self==nullptr);
A2DPStream_self = this;
info.bits_per_sample = 16;
info.sample_rate = 44100;
info.channels = 2;
}
/// Release the allocate a2dp_source or a2dp_sink
@ -137,9 +147,7 @@ class A2DPStream : public AudioStream {
this->config = cfg;
bool result = false;
LOGI("Connecting to %s",cfg.name);
if (a2dp_buffer==nullptr){
a2dp_buffer = new RingBuffer<uint8_t>(cfg.bufferSize);
}
a2dp_buffer.resize(cfg.bufferSize);
switch (cfg.mode){
case TX_MODE:
@ -149,16 +157,16 @@ class A2DPStream : public AudioStream {
a2dp_source->set_volume(volume * 100);
if(cfg.name=="[Unknown]"){
//search next available device
a2dp_source->set_ssid_callback(detectedDevice);
a2dp_source->set_ssid_callback(detected_device);
}
a2dp_source->set_on_connection_state_changed(a2dpStateCallback, this);
a2dp_source->start((char*)cfg.name, a2dp_stream_source_sound_data);
a2dp_source->set_on_connection_state_changed(a2dp_state_callback, this);
a2dp_source->start_raw((char*)cfg.name, a2dp_stream_source_sound_data);
while(!a2dp_source->is_connected()){
LOGD("waiting for connection");
delay(1000);
}
LOGI("a2dp_source is connected...");
notifyBaseInfo(44100);
notify_base_Info(44100);
//is_a2dp_active = true;
result = true;
break;
@ -169,7 +177,7 @@ class A2DPStream : public AudioStream {
a2dp_sink->set_auto_reconnect(cfg.auto_reconnect);
a2dp_sink->set_stream_reader(&a2dp_stream_sink_sound_data, false);
a2dp_sink->set_volume(volume * 100);
a2dp_sink->set_on_connection_state_changed(a2dpStateCallback, this);
a2dp_sink->set_on_connection_state_changed(a2dp_state_callback, this);
a2dp_sink->set_sample_rate_callback(sample_rate_callback);
a2dp_sink->start((char*)cfg.name);
while(!a2dp_sink->is_connected()){
@ -204,61 +212,51 @@ class A2DPStream : public AudioStream {
/// Writes the data into a temporary send buffer - where it can be picked up by the callback
virtual size_t write(const uint8_t* data, size_t len) {
if (a2dp_buffer==nullptr) return 0;
LOGD("%s: %zu", LOG_METHOD, len);
// blocking write - we wait for space in buffer
bool isBufferFull = true;
while(isBufferFull){
lockSemaphore(true);
isBufferFull = a2dp_buffer->availableForWrite()<len;
lockSemaphore(false);
// we wait until the buffer is full
if (isBufferFull){
if (!is_a2dp_active){
if (config.mode==TX_MODE){
// if buffer is full and we are still not connected, we wait
while(len > a2dp_buffer.availableForWrite()){
LOGI("waiting for buffer to be consumed...")
delay(200);
if (config.startLogic==StartWhenBufferFull){
is_a2dp_active = true;
LOGW("is_a2dp_active -> true with %d bytes", a2dp_buffer->available());
}
delay(100);
LOGD("Waiting for free buffer space - available: %d", a2dp_buffer->available());
}
}
// write to buffer
lockSemaphore(true);
size_t result = a2dp_buffer->writeArray(data, len);
lockSemaphore(false);
size_t result = a2dp_buffer.writeArray(data, len);
LOGD("write %d -> %d", len, result);
if (config.mode==TX_MODE){
// give the callback a chance to retrieve the data
delay(config.delay_ms);
}
return result;
}
/// Reads the data from the temporary buffer
virtual size_t readBytes(uint8_t *data, size_t len) {
if (a2dp_buffer==nullptr) return 0;
size_t result = 0;
if (is_a2dp_active){
LOGD("readBytes %d", len);
result = a2dp_buffer->readArray(data, len);
LOGI("readBytes %d->%d", len,result);
} else {
if (!is_a2dp_active){
LOGW( "readBytes failed because !is_a2dp_active");
return 0;
}
LOGD("readBytes %d", len);
size_t result = a2dp_buffer.readArray(data, len);
LOGI("readBytes %d->%d", len,result);
return result;
}
virtual int available() {
// only supported in tx mode
if (config.mode!=RX_MODE || a2dp_buffer==nullptr ) return 0;
return a2dp_buffer->available();
if (config.mode!=RX_MODE) return 0;
return a2dp_buffer.available();
}
virtual int availableForWrite() {
// only supported in tx mode
if (config.mode!=TX_MODE || a2dp_buffer==nullptr ) return 0;
if (config.mode!=TX_MODE ) return 0;
// return infor from buffer
return a2dp_buffer->availableForWrite();
return a2dp_buffer.availableForWrite();
}
// Define the volme (values between 0.0 and 1.0)
@ -280,17 +278,15 @@ class A2DPStream : public AudioStream {
BluetoothA2DPCommon *a2dp=nullptr;
AudioBaseInfoDependent *audioBaseInfoDependent=nullptr;
float volume = 1.0;
// semaphore to synchronize acess to the buffer
SemaphoreHandle_t xSemaphore = NULL;
// auto-detect device to send audio to (TX-Mode)
static bool detectedDevice(const char* ssid, esp_bd_addr_t address, int rssi){
static bool detected_device(const char* ssid, esp_bd_addr_t address, int rssi){
LOGW("found Device: %s rssi: %d", ssid, rssi);
//filter out weak signals
return (rssi > -75);
}
static void a2dpStateCallback(esp_a2d_connection_state_t state, void *caller){
static void a2dp_state_callback(esp_a2d_connection_state_t state, void *caller){
TRACED();
A2DPStream *self = (A2DPStream*)caller;
if (state==ESP_A2D_CONNECTION_STATE_CONNECTED && self->config.startLogic==StartOnConnect){
@ -299,71 +295,45 @@ class A2DPStream : public AudioStream {
LOGW("==> state: %s", self->a2dp->to_str(state));
}
bool lockSemaphore(bool locked, bool immediate=false){
bool result = false;
if (locked){
result = xSemaphoreTake( xSemaphore, immediate ? 10 : portMAX_DELAY ) == pdTRUE;
} else {
result = xSemaphoreGive( xSemaphore ) == pdTRUE;
}
return result;
}
// callback used by A2DP to provide the a2dp_source sound data
static int32_t a2dp_stream_source_sound_data(Frame* data, int32_t len) {
if (a2dp_buffer==nullptr) return 0;
static int32_t a2dp_stream_source_sound_data(uint8_t* data, int32_t len) {
int32_t result_len = 0;
bool isAvailable = false;
bool isLocked = false;
if (is_a2dp_active) {
if (A2DPStream_self->lockSemaphore(true)) {
isAvailable = a2dp_buffer->available()>0;
LOGD("buffer: %d, free %d",a2dp_buffer->available(), a2dp_buffer->availableForWrite() )
isLocked = true;
} else {
LOGE("lock failed");
}
}
A2DPConfig config = A2DPStream_self->config;
// at first call we start with some empty data
if (isAvailable){
if (is_a2dp_active){
// the data in the file must be in int16 with 2 channels
size_t result_len_bytes = a2dp_buffer->readArray((uint8_t*)data, len*sizeof(Frame));
A2DPStream_self->lockSemaphore(false);
// result is in number of frames
result_len = result_len_bytes / sizeof(Frame);
yield();
result_len = a2dp_buffer.readArray((uint8_t*)data, len);
} else {
if (isLocked) A2DPStream_self->lockSemaphore(false);
// prevent underflow on first call
switch (A2DPStream_self->config.noData) {
switch (config.noData) {
case A2DPSilence:
memset(data, 0, len*sizeof(Frame));
memset(data, 0, len);
break;
case A2DPWhoosh:
for (int j=0;j<len;j++){
data[j].channel2 = data[j].channel1 = (rand() % 50) - 25;
int16_t *data16 = (int16_t*)data;
for (int j=0;j<len/4;j+=2){
data16[j+1] = data16[j] = (rand() % 50) - 25;
}
break;
}
result_len = len;
delay(3);
// Priority: 22 on core 0
// LOGI("Priority: %d on core %d", uxTaskPriorityGet(NULL), xPortGetCoreID());
}
LOGD("a2dp_stream_source_sound_data: %d -> %d (%s)", len, result_len, isAvailable?"+":"-");
LOGD("a2dp_stream_source_sound_data: %d -> %d", len, result_len);
return result_len;
}
/// callback used by A2DP to write the sound data
static void a2dp_stream_sink_sound_data(const uint8_t* data, uint32_t len) {
if (is_a2dp_active && a2dp_buffer!=nullptr){
uint32_t result_len = a2dp_buffer->writeArray(data, len);
if (is_a2dp_active){
uint32_t result_len = a2dp_buffer.writeArray(data, len);
LOGD("a2dp_stream_sink_sound_data %d -> %d", len, result_len);
// allow some other task
//yield();
@ -371,7 +341,7 @@ class A2DPStream : public AudioStream {
}
/// notify subscriber with AudioBaseInfo
void notifyBaseInfo(int rate){
void notify_base_Info(int rate){
if (audioBaseInfoDependent!=nullptr){
AudioBaseInfo info;
info.channels = 2;
@ -384,7 +354,7 @@ class A2DPStream : public AudioStream {
/// callback to update audio info with used a2dp sample rate
static void sample_rate_callback(uint16_t rate) {
if (A2DPStream_self->audioBaseInfoDependent!=nullptr){
A2DPStream_self->notifyBaseInfo(rate);
A2DPStream_self->notify_base_Info(rate);
}
}

View File

@ -333,14 +333,24 @@ protected:
template <typename T>
class SynchronizedBufferRTOS : public BaseBuffer<T> {
public:
SynchronizedBufferRTOS(size_t xStreamBufferSizeBytes, size_t xTriggerLevel=256, int writeMaxWait=portMAX_DELAY, int readMaxWait=portMAX_DELAY)
SynchronizedBufferRTOS(size_t xStreamBufferSizeBytes, size_t xTriggerLevel=256, TickType_t writeMaxWait=portMAX_DELAY, TickType_t readMaxWait=portMAX_DELAY)
: BaseBuffer<T>() {
xStreamBuffer = xStreamBufferCreate(xStreamBufferSizeBytes, xTriggerLevel);
readWait = readMaxWait;
writeWait = writeMaxWait;
current_size = xStreamBufferSizeBytes;
trigger_level = xTriggerLevel;
}
~SynchronizedBufferRTOS() { vStreamBufferDelete(xStreamBuffer); }
void resize(size_t size){
if (current_size != size){
vStreamBufferDelete(xStreamBuffer);
xStreamBuffer = xStreamBufferCreate(size, trigger_level);
current_size = size;
}
}
void setReadMaxWait(TickType_t ticks){
readWait = ticks;
}
@ -441,6 +451,8 @@ protected:
int writeWait = portMAX_DELAY;
bool read_from_isr = false;
bool write_from_isr = false;
size_t current_size=0;
size_t trigger_level=0;
};
#endif // ESP_IDF_VERSION_MAJOR >= 4
#endif // ESP32