mirror of
https://github.com/pschatzmann/arduino-audio-tools.git
synced 2024-09-21 18:37:28 +00:00
A2DPStream Corrections
This commit is contained in:
parent
b201028a79
commit
9598c95ec7
@ -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();
|
||||
}
|
@ -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() {
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user