mirror of
https://github.com/pschatzmann/arduino-audio-tools.git
synced 2024-09-21 10:27:27 +00:00
rename to A2DPStream.h
This commit is contained in:
parent
110ae0ae2e
commit
5977668cc5
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h" // install https://github.com/pschatzmann/ESP32-A2DP
|
||||
#include "AudioLibs/A2DPStream.h" // install https://github.com/pschatzmann/ESP32-A2DP
|
||||
#include "AudioLibs/AudioBoardStream.h" // install https://github.com/pschatzmann/arduino-audio-driver
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h" // install https://github.com/pschatzmann/ESP32-A2DP
|
||||
#include "AudioLibs/A2DPStream.h" // install https://github.com/pschatzmann/ESP32-A2DP
|
||||
#include "AudioLibs/AudioBoardStream.h" // install https://github.com/pschatzmann/arduino-audio-driver
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioBoardStream.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
|
||||
AudioInfo info(44100, 2, 16);
|
||||
BluetoothA2DPSource a2dp_source;
|
||||
|
@ -12,7 +12,7 @@
|
||||
// install https://github.com/greiman/SdFat.git
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
#include "AudioLibs/AudioBoardStream.h"
|
||||
#include "AudioLibs/AudioSourceSDFAT.h"
|
||||
#include "AudioCodecs/CodecMP3Helix.h"
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
#include "AudioLibs/AudioBoardStream.h"
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.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)
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
|
||||
AudioInfo info32(44100, 2, 32);
|
||||
AudioInfo info16(44100, 2, 16);
|
||||
|
@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
#include "AudioLibs/AudioSourceSDFAT.h"
|
||||
#include "AudioCodecs/CodecMP3Helix.h"
|
||||
#include "AudioLibs/AudioBoardStream.h" // for SD Pins
|
||||
|
@ -1,15 +1,20 @@
|
||||
|
||||
/**
|
||||
* We use the default Arduino SPI API to send/write the data as SPI Master.
|
||||
*
|
||||
* For a sample rate of 44100 with 2 channels and 16 bit data you need to be
|
||||
* able to transmit faster then 44100 * 2 channels * 2 bytes = 176400 bytes per
|
||||
* second. Using a SPI communication this gives 176400 * 8 =
|
||||
* 1'411'200 bps!
|
||||
*
|
||||
* Untested DRAFT implementation!
|
||||
*/
|
||||
#include <SPI.h>
|
||||
|
||||
#include "AudioTools.h"
|
||||
|
||||
const size_t BUFFER_SIZE = 1024;
|
||||
const int SPI_CLOCK = 2000000; // 2 MHz
|
||||
AudioInfo info(44100, 2, 16); //
|
||||
AudioInfo info(44100, 2, 16); //
|
||||
Vector<uint8_t> buffer(BUFFER_SIZE);
|
||||
SineWaveGenerator<int16_t> sineWave(32000);
|
||||
GeneratedSoundStream<int16_t> sound(sineWave);
|
||||
@ -17,11 +22,13 @@ GeneratedSoundStream<int16_t> sound(sineWave);
|
||||
void setup() {
|
||||
SPI.begin();
|
||||
sineWave.begin(info, N_B4);
|
||||
SPI.beginTransaction(SPISettings(SPI_CLOCK, MSBFIRST, SPI_MODE0));
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// get data
|
||||
size_t result = sound.readBytes(&buffer[0], buffer.size());
|
||||
SPI.beginTransaction(SPISettings(SPI_CLOCK, MSBFIRST, SPI_MODE0));
|
||||
// transmit data
|
||||
SPI.transfer(&buffer[0], result);
|
||||
SPI.endTransaction();
|
||||
//SPI.endTransaction();
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Demo how to use the Mozzi API to provide a stream of int16_t data.
|
||||
* Inspired by https://sensorium.github.io/Mozzi/examples/#01.Basics
|
||||
*/
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
#include "AudioLibs/MozziStream.h"
|
||||
#include <Oscil.h> // oscillator template
|
||||
#include <tables/sin2048_int8.h> // sine table for oscillator
|
||||
|
||||
const int sample_rate = 44100;
|
||||
AudioInfo info(sample_rate, 2, 16); // bluetooth requires 44100, stereo, 16 bits
|
||||
A2DPStream out;
|
||||
MozziStream mozzi; // audio source
|
||||
StreamCopy copier(out, mozzi); // copy source to sink
|
||||
// use: Oscil <table_size, update_rate> oscilName (wavetable), look in .h file
|
||||
// of table #included above
|
||||
Oscil<SIN2048_NUM_CELLS, sample_rate> aSin(SIN2048_DATA);
|
||||
// control variable, use the smallest data size you can for anything used in
|
||||
// audio
|
||||
byte gain = 255;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
AudioLogger::instance().begin(Serial, AudioLogger::Info);
|
||||
|
||||
// setup mozzi
|
||||
auto cfg = mozzi.defaultConfig();
|
||||
cfg.control_rate = CONTROL_RATE;
|
||||
cfg.copyFrom(info);
|
||||
mozzi.begin(cfg);
|
||||
|
||||
// setup output
|
||||
out.begin(TX_MODE, "Mozzi");
|
||||
|
||||
// setup mozzi sine
|
||||
aSin.setFreq(3320); // set the frequency
|
||||
}
|
||||
|
||||
void loop() { copier.copy(); }
|
||||
|
||||
void updateControl() {
|
||||
// as byte, this will automatically roll around to 255 when it passes 0
|
||||
gain = gain - 3;
|
||||
}
|
||||
|
||||
int updateAudio() {
|
||||
return (aSin.next() * gain) >>
|
||||
8; // shift back to STANDARD audio range, like /256 but faster
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Demo how to use the Mozzi API to provide a stream on int16_t data.
|
||||
* Demo how to use the Mozzi API to provide a stream of int16_t data.
|
||||
* Inspired by https://sensorium.github.io/Mozzi/examples/#01.Basics
|
||||
*/
|
||||
#include "AudioTools.h"
|
||||
|
@ -10,7 +10,7 @@
|
||||
#define HELIX_LOGGING_ACTIVE false
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
#include "AudioLibs/AudioSourceSDFAT.h"
|
||||
#include "AudioCodecs/CodecMP3Helix.h"
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
|
||||
|
||||
A2DPStream in;
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
|
||||
const char* name = "LEXON MINO L"; // Replace with your device name
|
||||
AudioInfo info(44100, 2, 16);
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
|
||||
I2SStream i2sStream; // Access I2S as stream
|
||||
A2DPStream a2dpStream; // access A2DP as stream
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "AudioCodecs/CodecMP3Helix.h"
|
||||
#include "AudioLibs/AudioA2DP.h"
|
||||
#include "AudioLibs/A2DPStream.h"
|
||||
#include "SimpleTTS.h"
|
||||
|
||||
const char* name = "LEXON MINO L"; // Replace with your device name
|
||||
|
354
src/AudioLibs/A2DPStream.h
Normal file
354
src/AudioLibs/A2DPStream.h
Normal file
@ -0,0 +1,354 @@
|
||||
/**
|
||||
* @file A2DPStream.h
|
||||
* @author Phil Schatzmann
|
||||
* @brief A2DP Support via Arduino Streams
|
||||
* @copyright GPLv3
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "AudioConfig.h"
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "BluetoothA2DPSink.h"
|
||||
#include "BluetoothA2DPSource.h"
|
||||
#include "AudioTools/AudioStreams.h"
|
||||
#include "Concurrency/BufferRTOS.h"
|
||||
|
||||
|
||||
namespace audio_tools {
|
||||
|
||||
class A2DPStream;
|
||||
static A2DPStream *A2DPStream_self=nullptr;
|
||||
// buffer which is used to exchange data
|
||||
static BufferRTOS<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;
|
||||
|
||||
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);
|
||||
|
||||
enum A2DPStartLogic {StartWhenBufferFull, StartOnConnect};
|
||||
enum A2DPNoData {A2DPSilence, A2DPWhoosh};
|
||||
|
||||
/**
|
||||
* @brief Configuration for A2DPStream
|
||||
* @author Phil Schatzmann
|
||||
* @copyright GPLv3
|
||||
*/
|
||||
class A2DPConfig {
|
||||
public:
|
||||
A2DPStartLogic startLogic = StartWhenBufferFull;
|
||||
A2DPNoData noData = A2DPSilence;
|
||||
RxTxMode mode = RX_MODE;
|
||||
const char* name = "A2DP";
|
||||
bool auto_reconnect = false;
|
||||
int bufferSize = A2DP_BUFFER_SIZE * A2DP_BUFFER_COUNT;
|
||||
int delay_ms = 1;
|
||||
/// when a2dp source has no data we generate silence data
|
||||
bool silence_on_nodata = false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* Requires: https://github.com/pschatzmann/ESP32-A2DP
|
||||
*
|
||||
* @ingroup io
|
||||
* @ingroup communications
|
||||
* @author Phil Schatzmann
|
||||
* @copyright GPLv3
|
||||
*/
|
||||
class A2DPStream : public AudioStream, public VolumeSupport {
|
||||
|
||||
public:
|
||||
A2DPStream() {
|
||||
TRACED();
|
||||
// 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
|
||||
~A2DPStream(){
|
||||
TRACED();
|
||||
if (a2dp_source!=nullptr) delete a2dp_source;
|
||||
if (a2dp_sink!=nullptr) delete a2dp_sink;
|
||||
A2DPStream_self = nullptr;
|
||||
}
|
||||
|
||||
A2DPConfig defaultConfig(RxTxMode mode=RX_MODE){
|
||||
A2DPConfig cfg;
|
||||
cfg.mode = mode;
|
||||
if(mode==TX_MODE){
|
||||
cfg.name="[Unknown]";
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/// provides access to the
|
||||
BluetoothA2DPSource &source() {
|
||||
if (a2dp_source==nullptr){
|
||||
a2dp = a2dp_source = new BluetoothA2DPSource();
|
||||
}
|
||||
return *a2dp_source;
|
||||
}
|
||||
|
||||
/// provides access to the BluetoothA2DPSink
|
||||
BluetoothA2DPSink &sink(){
|
||||
if (a2dp_sink==nullptr){
|
||||
a2dp = a2dp_sink = new BluetoothA2DPSink();
|
||||
}
|
||||
return *a2dp_sink;
|
||||
}
|
||||
|
||||
/// Starts the processing
|
||||
bool begin(RxTxMode mode, const char* name){
|
||||
A2DPConfig cfg;
|
||||
cfg.mode = mode;
|
||||
cfg.name = name;
|
||||
return begin(cfg);
|
||||
}
|
||||
|
||||
/// Starts the processing
|
||||
bool begin(A2DPConfig cfg){
|
||||
this->config = cfg;
|
||||
bool result = false;
|
||||
LOGI("Connecting to %s",cfg.name);
|
||||
a2dp_buffer.resize(cfg.bufferSize);
|
||||
|
||||
// initialize a2dp_silence_timeout
|
||||
if (config.silence_on_nodata){
|
||||
LOGI("Using StartOnConnect")
|
||||
config.startLogic = StartOnConnect;
|
||||
}
|
||||
|
||||
switch (cfg.mode){
|
||||
case TX_MODE:
|
||||
LOGI("Starting a2dp_source...");
|
||||
source(); // allocate object
|
||||
a2dp_source->set_auto_reconnect(cfg.auto_reconnect);
|
||||
a2dp_source->set_volume(volume() * A2DP_MAX_VOL);
|
||||
if(Str(cfg.name).equals("[Unknown]")){
|
||||
//search next available device
|
||||
a2dp_source->set_ssid_callback(detected_device);
|
||||
}
|
||||
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...");
|
||||
notify_base_Info(44100);
|
||||
//is_a2dp_active = true;
|
||||
result = true;
|
||||
break;
|
||||
|
||||
case RX_MODE:
|
||||
LOGI("Starting a2dp_sink...");
|
||||
sink(); // allocate object
|
||||
a2dp_sink->set_auto_reconnect(cfg.auto_reconnect);
|
||||
a2dp_sink->set_stream_reader(&a2dp_stream_sink_sound_data, false);
|
||||
a2dp_sink->set_volume(volume() * A2DP_MAX_VOL);
|
||||
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()){
|
||||
LOGD("waiting for connection");
|
||||
delay(1000);
|
||||
}
|
||||
LOGI("a2dp_sink is connected...");
|
||||
is_a2dp_active = true;
|
||||
result = true;
|
||||
break;
|
||||
default:
|
||||
LOGE("Undefined mode: %d", cfg.mode);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void end() override {
|
||||
if (a2dp != nullptr) {
|
||||
a2dp->disconnect();
|
||||
}
|
||||
AudioStream::end();
|
||||
}
|
||||
|
||||
/// checks if we are connected
|
||||
bool isConnected() {
|
||||
if (a2dp_source==nullptr && a2dp_sink==nullptr) return false;
|
||||
if (a2dp_source!=nullptr) return a2dp_source->is_connected();
|
||||
return a2dp_sink->is_connected();
|
||||
}
|
||||
|
||||
/// is ready to process data
|
||||
bool isReady() {
|
||||
return is_a2dp_active;
|
||||
}
|
||||
|
||||
/// convert to bool
|
||||
operator bool() {
|
||||
return isReady();
|
||||
}
|
||||
|
||||
/// Writes the data into a temporary send buffer - where it can be picked up by the callback
|
||||
size_t write(const uint8_t* data, size_t len) override {
|
||||
LOGD("%s: %zu", LOG_METHOD, len);
|
||||
|
||||
if (config.mode==TX_MODE){
|
||||
// if buffer is full we wait
|
||||
while(len > a2dp_buffer.availableForWrite()){
|
||||
LOGD("Waiting for buffer to be available");
|
||||
delay(5);
|
||||
if (config.startLogic==StartWhenBufferFull){
|
||||
is_a2dp_active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write to buffer
|
||||
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
|
||||
size_t readBytes(uint8_t *data, size_t len) override {
|
||||
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;
|
||||
}
|
||||
|
||||
int available() override {
|
||||
// only supported in tx mode
|
||||
if (config.mode!=RX_MODE) return 0;
|
||||
return a2dp_buffer.available();
|
||||
}
|
||||
|
||||
int availableForWrite() override {
|
||||
// only supported in tx mode
|
||||
if (config.mode!=TX_MODE ) return 0;
|
||||
// return infor from buffer
|
||||
return a2dp_buffer.availableForWrite();
|
||||
}
|
||||
|
||||
// Define the volme (values between 0.0 and 1.0)
|
||||
bool setVolume(float volume) override {
|
||||
VolumeSupport::setVolume(volume);
|
||||
// 128 is max volume
|
||||
if (a2dp!=nullptr) a2dp->set_volume(volume * A2DP_MAX_VOL);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
A2DPConfig config;
|
||||
BluetoothA2DPSource *a2dp_source = nullptr;
|
||||
BluetoothA2DPSink *a2dp_sink = nullptr;
|
||||
BluetoothA2DPCommon *a2dp=nullptr;
|
||||
const int A2DP_MAX_VOL = 128;
|
||||
|
||||
// auto-detect device to send audio to (TX-Mode)
|
||||
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 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){
|
||||
is_a2dp_active = true;
|
||||
}
|
||||
LOGW("==> state: %s", self->a2dp->to_str(state));
|
||||
}
|
||||
|
||||
|
||||
// callback used by A2DP to provide the a2dp_source sound data
|
||||
static int32_t a2dp_stream_source_sound_data(uint8_t* data, int32_t len) {
|
||||
int32_t result_len = 0;
|
||||
A2DPConfig config = A2DPStream_self->config;
|
||||
|
||||
// at first call we start with some empty data
|
||||
if (is_a2dp_active){
|
||||
// the data in the file must be in int16 with 2 channels
|
||||
yield();
|
||||
result_len = a2dp_buffer.readArray((uint8_t*)data, len);
|
||||
|
||||
// provide silence data
|
||||
if (config.silence_on_nodata && result_len == 0){
|
||||
memset(data,0, len);
|
||||
result_len = len;
|
||||
}
|
||||
} else {
|
||||
|
||||
// prevent underflow on first call
|
||||
switch (config.noData) {
|
||||
case A2DPSilence:
|
||||
memset(data, 0, len);
|
||||
break;
|
||||
case A2DPWhoosh:
|
||||
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;
|
||||
|
||||
// Priority: 22 on core 0
|
||||
// LOGI("Priority: %d on core %d", uxTaskPriorityGet(NULL), xPortGetCoreID());
|
||||
|
||||
}
|
||||
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){
|
||||
uint32_t result_len = a2dp_buffer.writeArray(data, len);
|
||||
LOGD("a2dp_stream_sink_sound_data %d -> %d", len, result_len);
|
||||
}
|
||||
}
|
||||
|
||||
/// notify subscriber with AudioInfo
|
||||
void notify_base_Info(int rate){
|
||||
AudioInfo info;
|
||||
info.channels = 2;
|
||||
info.bits_per_sample = 16;
|
||||
info.sample_rate = rate;
|
||||
notifyAudioChange(info);
|
||||
}
|
||||
|
||||
/// callback to update audio info with used a2dp sample rate
|
||||
static void sample_rate_callback(uint16_t rate) {
|
||||
A2DPStream_self->info.sample_rate = rate;
|
||||
A2DPStream_self->notify_base_Info(rate);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -1,354 +1,3 @@
|
||||
/**
|
||||
* @file AudioA2DP.h
|
||||
* @author Phil Schatzmann
|
||||
* @brief A2DP Support via Arduino Streams
|
||||
* @copyright GPLv3
|
||||
*
|
||||
*/
|
||||
// legacy include
|
||||
#pragma once
|
||||
|
||||
#include "AudioConfig.h"
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "BluetoothA2DPSink.h"
|
||||
#include "BluetoothA2DPSource.h"
|
||||
#include "AudioTools/AudioStreams.h"
|
||||
#include "Concurrency/BufferRTOS.h"
|
||||
|
||||
|
||||
namespace audio_tools {
|
||||
|
||||
class A2DPStream;
|
||||
static A2DPStream *A2DPStream_self=nullptr;
|
||||
// buffer which is used to exchange data
|
||||
static BufferRTOS<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;
|
||||
|
||||
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);
|
||||
|
||||
enum A2DPStartLogic {StartWhenBufferFull, StartOnConnect};
|
||||
enum A2DPNoData {A2DPSilence, A2DPWhoosh};
|
||||
|
||||
/**
|
||||
* @brief Configuration for A2DPStream
|
||||
* @author Phil Schatzmann
|
||||
* @copyright GPLv3
|
||||
*/
|
||||
class A2DPConfig {
|
||||
public:
|
||||
A2DPStartLogic startLogic = StartWhenBufferFull;
|
||||
A2DPNoData noData = A2DPSilence;
|
||||
RxTxMode mode = RX_MODE;
|
||||
const char* name = "A2DP";
|
||||
bool auto_reconnect = false;
|
||||
int bufferSize = A2DP_BUFFER_SIZE * A2DP_BUFFER_COUNT;
|
||||
int delay_ms = 1;
|
||||
/// when a2dp source has no data we generate silence data
|
||||
bool silence_on_nodata = false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* Requires: https://github.com/pschatzmann/ESP32-A2DP
|
||||
*
|
||||
* @ingroup io
|
||||
* @ingroup communications
|
||||
* @author Phil Schatzmann
|
||||
* @copyright GPLv3
|
||||
*/
|
||||
class A2DPStream : public AudioStream, public VolumeSupport {
|
||||
|
||||
public:
|
||||
A2DPStream() {
|
||||
TRACED();
|
||||
// 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
|
||||
~A2DPStream(){
|
||||
TRACED();
|
||||
if (a2dp_source!=nullptr) delete a2dp_source;
|
||||
if (a2dp_sink!=nullptr) delete a2dp_sink;
|
||||
A2DPStream_self = nullptr;
|
||||
}
|
||||
|
||||
A2DPConfig defaultConfig(RxTxMode mode=RX_MODE){
|
||||
A2DPConfig cfg;
|
||||
cfg.mode = mode;
|
||||
if(mode==TX_MODE){
|
||||
cfg.name="[Unknown]";
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/// provides access to the
|
||||
BluetoothA2DPSource &source() {
|
||||
if (a2dp_source==nullptr){
|
||||
a2dp = a2dp_source = new BluetoothA2DPSource();
|
||||
}
|
||||
return *a2dp_source;
|
||||
}
|
||||
|
||||
/// provides access to the BluetoothA2DPSink
|
||||
BluetoothA2DPSink &sink(){
|
||||
if (a2dp_sink==nullptr){
|
||||
a2dp = a2dp_sink = new BluetoothA2DPSink();
|
||||
}
|
||||
return *a2dp_sink;
|
||||
}
|
||||
|
||||
/// Starts the processing
|
||||
bool begin(RxTxMode mode, const char* name){
|
||||
A2DPConfig cfg;
|
||||
cfg.mode = mode;
|
||||
cfg.name = name;
|
||||
return begin(cfg);
|
||||
}
|
||||
|
||||
/// Starts the processing
|
||||
bool begin(A2DPConfig cfg){
|
||||
this->config = cfg;
|
||||
bool result = false;
|
||||
LOGI("Connecting to %s",cfg.name);
|
||||
a2dp_buffer.resize(cfg.bufferSize);
|
||||
|
||||
// initialize a2dp_silence_timeout
|
||||
if (config.silence_on_nodata){
|
||||
LOGI("Using StartOnConnect")
|
||||
config.startLogic = StartOnConnect;
|
||||
}
|
||||
|
||||
switch (cfg.mode){
|
||||
case TX_MODE:
|
||||
LOGI("Starting a2dp_source...");
|
||||
source(); // allocate object
|
||||
a2dp_source->set_auto_reconnect(cfg.auto_reconnect);
|
||||
a2dp_source->set_volume(volume() * A2DP_MAX_VOL);
|
||||
if(Str(cfg.name).equals("[Unknown]")){
|
||||
//search next available device
|
||||
a2dp_source->set_ssid_callback(detected_device);
|
||||
}
|
||||
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...");
|
||||
notify_base_Info(44100);
|
||||
//is_a2dp_active = true;
|
||||
result = true;
|
||||
break;
|
||||
|
||||
case RX_MODE:
|
||||
LOGI("Starting a2dp_sink...");
|
||||
sink(); // allocate object
|
||||
a2dp_sink->set_auto_reconnect(cfg.auto_reconnect);
|
||||
a2dp_sink->set_stream_reader(&a2dp_stream_sink_sound_data, false);
|
||||
a2dp_sink->set_volume(volume() * A2DP_MAX_VOL);
|
||||
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()){
|
||||
LOGD("waiting for connection");
|
||||
delay(1000);
|
||||
}
|
||||
LOGI("a2dp_sink is connected...");
|
||||
is_a2dp_active = true;
|
||||
result = true;
|
||||
break;
|
||||
default:
|
||||
LOGE("Undefined mode: %d", cfg.mode);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void end() override {
|
||||
if (a2dp != nullptr) {
|
||||
a2dp->disconnect();
|
||||
}
|
||||
AudioStream::end();
|
||||
}
|
||||
|
||||
/// checks if we are connected
|
||||
bool isConnected() {
|
||||
if (a2dp_source==nullptr && a2dp_sink==nullptr) return false;
|
||||
if (a2dp_source!=nullptr) return a2dp_source->is_connected();
|
||||
return a2dp_sink->is_connected();
|
||||
}
|
||||
|
||||
/// is ready to process data
|
||||
bool isReady() {
|
||||
return is_a2dp_active;
|
||||
}
|
||||
|
||||
/// convert to bool
|
||||
operator bool() {
|
||||
return isReady();
|
||||
}
|
||||
|
||||
/// Writes the data into a temporary send buffer - where it can be picked up by the callback
|
||||
size_t write(const uint8_t* data, size_t len) override {
|
||||
LOGD("%s: %zu", LOG_METHOD, len);
|
||||
|
||||
if (config.mode==TX_MODE){
|
||||
// if buffer is full we wait
|
||||
while(len > a2dp_buffer.availableForWrite()){
|
||||
LOGD("Waiting for buffer to be available");
|
||||
delay(5);
|
||||
if (config.startLogic==StartWhenBufferFull){
|
||||
is_a2dp_active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write to buffer
|
||||
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
|
||||
size_t readBytes(uint8_t *data, size_t len) override {
|
||||
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;
|
||||
}
|
||||
|
||||
int available() override {
|
||||
// only supported in tx mode
|
||||
if (config.mode!=RX_MODE) return 0;
|
||||
return a2dp_buffer.available();
|
||||
}
|
||||
|
||||
int availableForWrite() override {
|
||||
// only supported in tx mode
|
||||
if (config.mode!=TX_MODE ) return 0;
|
||||
// return infor from buffer
|
||||
return a2dp_buffer.availableForWrite();
|
||||
}
|
||||
|
||||
// Define the volme (values between 0.0 and 1.0)
|
||||
bool setVolume(float volume) override {
|
||||
VolumeSupport::setVolume(volume);
|
||||
// 128 is max volume
|
||||
if (a2dp!=nullptr) a2dp->set_volume(volume * A2DP_MAX_VOL);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
A2DPConfig config;
|
||||
BluetoothA2DPSource *a2dp_source = nullptr;
|
||||
BluetoothA2DPSink *a2dp_sink = nullptr;
|
||||
BluetoothA2DPCommon *a2dp=nullptr;
|
||||
const int A2DP_MAX_VOL = 128;
|
||||
|
||||
// auto-detect device to send audio to (TX-Mode)
|
||||
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 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){
|
||||
is_a2dp_active = true;
|
||||
}
|
||||
LOGW("==> state: %s", self->a2dp->to_str(state));
|
||||
}
|
||||
|
||||
|
||||
// callback used by A2DP to provide the a2dp_source sound data
|
||||
static int32_t a2dp_stream_source_sound_data(uint8_t* data, int32_t len) {
|
||||
int32_t result_len = 0;
|
||||
A2DPConfig config = A2DPStream_self->config;
|
||||
|
||||
// at first call we start with some empty data
|
||||
if (is_a2dp_active){
|
||||
// the data in the file must be in int16 with 2 channels
|
||||
yield();
|
||||
result_len = a2dp_buffer.readArray((uint8_t*)data, len);
|
||||
|
||||
// provide silence data
|
||||
if (config.silence_on_nodata && result_len == 0){
|
||||
memset(data,0, len);
|
||||
result_len = len;
|
||||
}
|
||||
} else {
|
||||
|
||||
// prevent underflow on first call
|
||||
switch (config.noData) {
|
||||
case A2DPSilence:
|
||||
memset(data, 0, len);
|
||||
break;
|
||||
case A2DPWhoosh:
|
||||
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;
|
||||
|
||||
// Priority: 22 on core 0
|
||||
// LOGI("Priority: %d on core %d", uxTaskPriorityGet(NULL), xPortGetCoreID());
|
||||
|
||||
}
|
||||
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){
|
||||
uint32_t result_len = a2dp_buffer.writeArray(data, len);
|
||||
LOGD("a2dp_stream_sink_sound_data %d -> %d", len, result_len);
|
||||
}
|
||||
}
|
||||
|
||||
/// notify subscriber with AudioInfo
|
||||
void notify_base_Info(int rate){
|
||||
AudioInfo info;
|
||||
info.channels = 2;
|
||||
info.bits_per_sample = 16;
|
||||
info.sample_rate = rate;
|
||||
notifyAudioChange(info);
|
||||
}
|
||||
|
||||
/// callback to update audio info with used a2dp sample rate
|
||||
static void sample_rate_callback(uint16_t rate) {
|
||||
A2DPStream_self->info.sample_rate = rate;
|
||||
A2DPStream_self->notify_base_Info(rate);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
#include "A2DPStream.h"
|
@ -65,7 +65,7 @@ class MozziStream : public AudioStream, public VolumeSupport {
|
||||
void end() { active = false; }
|
||||
|
||||
/// Defines the multiplication factor to scale the Mozzi value range to int16_t
|
||||
void setVolume(int16_t vol) {
|
||||
bool setVolume(int16_t vol) {
|
||||
cfg.output_volume = vol;
|
||||
return VolumeSupport::setVolume(vol);
|
||||
}
|
||||
|
@ -109,10 +109,11 @@ public:
|
||||
}
|
||||
|
||||
/// Sets both input and output volume value (from 0 to 1.0)
|
||||
void setVolume(float vol){
|
||||
bool setVolume(float vol){
|
||||
// make sure that value is between 0 and 1
|
||||
setVolumeIn(vol);
|
||||
setVolumeOut(vol);
|
||||
return true;
|
||||
}
|
||||
|
||||
void setVolumeIn(float vol){
|
||||
|
Loading…
Reference in New Issue
Block a user