Add pre-packaged dependency libraries (ESP32-audioI2S and PNGdec)
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <driver/i2s_std.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
|
||||
class AudioResampleBuffer {
|
||||
public:
|
||||
static constexpr size_t FIFO_SIZE_BYTES = 16384; // Muss Vielfaches von 4 sein (Stereo, 16 Bit)
|
||||
static constexpr size_t I2S_BLOCK_SIZE = 512; // DMA Blockgröße
|
||||
|
||||
|
||||
AudioResampleBuffer()
|
||||
:fifoWrite(0), fifoRead(0), m_resampleCursor(0.0f) {
|
||||
memset(fifo, 0, sizeof(fifo));
|
||||
memset(m_inputHistory, 0, sizeof(m_inputHistory));
|
||||
}
|
||||
void setChannelHandle(i2s_chan_handle_t i2sHandle){
|
||||
m_i2s = i2sHandle;
|
||||
}
|
||||
|
||||
// Set the input samplerates (updated by the LRCK monitoring)
|
||||
void setInputSamplerate(uint32_t samplerate) {
|
||||
if (samplerate == 8000 || samplerate == 22050 || samplerate == 44100 || samplerate == 48000) {
|
||||
m_sampleRate = samplerate;
|
||||
ESP_LOGI("ResampleBuffer", "Input samplerate set to %u Hz", samplerate);
|
||||
} else {
|
||||
ESP_LOGW("ResampleBuffer", "Invalid samplerate %u Hz, defaulting to 44100 Hz", samplerate);
|
||||
m_sampleRate = 44100;
|
||||
}
|
||||
}
|
||||
|
||||
// Muss zyklisch aufgerufen werden (z. B. aus Task)
|
||||
void loopResample() {
|
||||
alignas(4) uint8_t i2sBuf[I2S_BLOCK_SIZE];
|
||||
size_t bytesRead = 0;
|
||||
if (i2s_channel_read(m_i2s, i2sBuf, I2S_BLOCK_SIZE, &bytesRead, 50) != ESP_OK || bytesRead == 0)
|
||||
return;
|
||||
|
||||
size_t inSamples = bytesRead / 4; // Stereo, 16 Bit
|
||||
int16_t* inData = reinterpret_cast<int16_t*>(i2sBuf);
|
||||
|
||||
int16_t resampled[1024];
|
||||
size_t outSamples = resampleTo441Stereo(inData, inSamples, resampled);
|
||||
size_t outBytes = outSamples * 4;
|
||||
|
||||
if (fifoFree() >= outBytes) {
|
||||
fifoWriteBytes(reinterpret_cast<uint8_t*>(resampled), outBytes);
|
||||
} else {
|
||||
vTaskDelay(100);
|
||||
// ESP_LOGW("ResampleBuffer", "FIFO voll, Daten verworfen %i Bytes", outBytes - fifoFree());
|
||||
}
|
||||
}
|
||||
|
||||
// Bluetooth Callback: muss exakt "bytes" liefern
|
||||
int32_t getData(uint8_t* data, int32_t bytes) {
|
||||
while (fifoAvailable() < static_cast<size_t>(bytes)) {
|
||||
vTaskDelay(1);
|
||||
}
|
||||
fifoReadBytes(data, bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private:
|
||||
private:
|
||||
i2s_chan_handle_t m_i2s;
|
||||
uint8_t fifo[FIFO_SIZE_BYTES];
|
||||
size_t fifoWrite;
|
||||
size_t fifoRead;
|
||||
float m_sampleRate = 44100.0f;
|
||||
|
||||
float m_resampleCursor;
|
||||
int16_t m_inputHistory[6]; // 3 Stereo-Samples
|
||||
|
||||
size_t fifoAvailable() const {
|
||||
return (fifoWrite + FIFO_SIZE_BYTES - fifoRead) % FIFO_SIZE_BYTES;
|
||||
}
|
||||
|
||||
size_t fifoFree() const {
|
||||
return FIFO_SIZE_BYTES - fifoAvailable() - 1;
|
||||
}
|
||||
|
||||
void fifoWriteBytes(const uint8_t* data, size_t len) {
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
fifo[fifoWrite] = data[i];
|
||||
fifoWrite = (fifoWrite + 1) % FIFO_SIZE_BYTES;
|
||||
}
|
||||
}
|
||||
|
||||
void fifoReadBytes(uint8_t* out, size_t len) {
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
out[i] = fifo[fifoRead];
|
||||
fifoRead = (fifoRead + 1) % FIFO_SIZE_BYTES;
|
||||
}
|
||||
}
|
||||
|
||||
// Catmull-Rom Spline Resampling von 48 kHz auf 44,1 kHz
|
||||
size_t resampleTo441Stereo(const int16_t* input, size_t inputSamples, int16_t* output) {
|
||||
float ratio = m_sampleRate / 44100.0f;
|
||||
float cursor = m_resampleCursor;
|
||||
|
||||
size_t extendedSamples = inputSamples + 3;
|
||||
std::vector<int16_t> extendedInput(extendedSamples * 2);
|
||||
|
||||
memcpy(&extendedInput[0], m_inputHistory, 6 * sizeof(int16_t));
|
||||
memcpy(&extendedInput[6], input, inputSamples * 2 * sizeof(int16_t));
|
||||
|
||||
size_t outputIndex = 0;
|
||||
|
||||
auto catmullRom = [](float t, float xm1, float x0, float x1, float x2) {
|
||||
return 0.5f * (
|
||||
(2.0f * x0) +
|
||||
(-xm1 + x1) * t +
|
||||
(2.0f * xm1 - 5.0f * x0 + 4.0f * x1 - x2) * t * t +
|
||||
(-xm1 + 3.0f * x0 - 3.0f * x1 + x2) * t * t * t
|
||||
);
|
||||
};
|
||||
|
||||
auto clip = [](float v) -> int16_t {
|
||||
return v > 32767.0f ? 32767 : (v < -32768.0f ? -32768 : static_cast<int16_t>(v));
|
||||
};
|
||||
|
||||
for (size_t inIdx = 1; inIdx < extendedSamples - 2; ++inIdx) {
|
||||
int16_t xm1_l = extendedInput[(inIdx - 1) * 2];
|
||||
int16_t x0_l = extendedInput[(inIdx + 0) * 2];
|
||||
int16_t x1_l = extendedInput[(inIdx + 1) * 2];
|
||||
int16_t x2_l = extendedInput[(inIdx + 2) * 2];
|
||||
|
||||
int16_t xm1_r = extendedInput[(inIdx - 1) * 2 + 1];
|
||||
int16_t x0_r = extendedInput[(inIdx + 0) * 2 + 1];
|
||||
int16_t x1_r = extendedInput[(inIdx + 1) * 2 + 1];
|
||||
int16_t x2_r = extendedInput[(inIdx + 2) * 2 + 1];
|
||||
|
||||
while (cursor < 1.0f) {
|
||||
float t = cursor;
|
||||
output[outputIndex * 2] = clip(catmullRom(t, xm1_l, x0_l, x1_l, x2_l));
|
||||
output[outputIndex * 2 + 1] = clip(catmullRom(t, xm1_r, x0_r, x1_r, x2_r));
|
||||
++outputIndex;
|
||||
cursor += ratio;
|
||||
}
|
||||
cursor -= 1.0f;
|
||||
}
|
||||
|
||||
// Historie sichern
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
size_t idx = inputSamples - 3 + i;
|
||||
m_inputHistory[i * 2] = input[idx * 2];
|
||||
m_inputHistory[i * 2 + 1] = input[idx * 2 + 1];
|
||||
}
|
||||
|
||||
m_resampleCursor = cursor;
|
||||
return outputIndex;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
**I2S Bluetooth Transmitter**
|
||||
|
||||
Such boards can be connected directly to the I2S output: <br>
|
||||
But to do this, `#define SR_48K` must be activated in Audio.h so that the I2S frequency is always 48KHz.
|
||||
|
||||

|
||||
|
||||
If you still have an old ESP32 in your box, you can use it to simulate this board. PSRAM is not required.
|
||||
|
||||
The BT transmitter is the slave and is connected in this way, the DAC serves as an analogue output, but is not necessary.
|
||||
|
||||

|
||||
|
||||
|
||||
The ESP32-A2DP library is used by P. Schatzmann, https://github.com/pschatzmann/ESP32-A2DP.git
|
||||
|
||||
As the I2S output of the audioI2S library not always outputs 44.1KHz, it is scaled internally to 44.1KHz for compatibility. 8000Hz, 22050Hz, 44100Hz and 48000Hz are possible.
|
||||
This is necessary because the ESP32 BT library expects this sample rate. This means that old BT devices can also be used.
|
||||
It doesn't matter what sample rate the audio source has.
|
||||
|
||||

|
||||
|
||||
Test circuit: the audioI2S library is running on the left, the BT transmitter on the right
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
#include "Arduino.h"
|
||||
/*
|
||||
I2S Bluetooth Transmitter
|
||||
Can be connected to an I2S Master.Sampling rate must be 48KHz
|
||||
Include PSchatzmann /ESP32-A2DP https://github.com/pschatzmann/ESP32-A2DP.git
|
||||
*/
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "esp_bt.h"
|
||||
#include "BluetoothA2DPCommon.h"
|
||||
#include "BluetoothA2DPSource.h"
|
||||
#include <driver/i2s_std.h>
|
||||
#include "AudioResampler.hpp"
|
||||
|
||||
#define RX_I2S_DIN 25 // connect with I2S Master (signal dout)
|
||||
#define RX_I2S_BCLK 27 // connect with I2S Master (bit clock)
|
||||
#define RX_I2S_LRC 26 // connect with I2S Master (word select)
|
||||
|
||||
#define MEASUREMENT_INTERVAL_MS 100 // Messintervall in ms
|
||||
#define TOLERANCE_PERCENT 5 // Toleranz für Schwankungen in %
|
||||
static volatile uint32_t lrck_count = 0;
|
||||
static uint32_t current_samplerate = 0;
|
||||
|
||||
BluetoothA2DPSource a2dp_source;
|
||||
AudioResampleBuffer resampler;
|
||||
|
||||
char BT_SINK_NAME[] = " Pebble V3\r\n"; // set your sink devicename here
|
||||
//char BT_SINK_NAME[] = "Manhattan-165327";
|
||||
|
||||
i2s_chan_handle_t i2s_rx_handle = {};
|
||||
i2s_chan_config_t i2s_chan_cfg = {}; // stores I2S channel values
|
||||
i2s_std_config_t i2s_std_cfg = {}; // stores I2S driver values
|
||||
|
||||
const i2s_port_t i2s_num = I2S_NUM_0;
|
||||
|
||||
//--------------------------------Recognise Host Samplerate-------------------------------------------------------------------------------------
|
||||
// Interrupt-Handler for LRCK-Pulse
|
||||
void IRAM_ATTR lrck_interrupt_handler(void *arg) {
|
||||
lrck_count+=1;
|
||||
}
|
||||
|
||||
// Initialize GPIO interrupt for LRCK
|
||||
void init_lrck_monitor() {
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << RX_I2S_LRC),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_POSEDGE // Interrupt bei steigender Flanke
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
gpio_install_isr_service(0);
|
||||
gpio_isr_handler_add((gpio_num_t)RX_I2S_LRC, lrck_interrupt_handler, NULL);
|
||||
}
|
||||
|
||||
// calculate samplerates based on LRCK counter
|
||||
uint32_t measure_samplerate() {
|
||||
lrck_count = 0;
|
||||
vTaskDelay(MEASUREMENT_INTERVAL_MS / portTICK_PERIOD_MS);
|
||||
uint32_t samplerate = (lrck_count * 1000) / MEASUREMENT_INTERVAL_MS; // Hz
|
||||
return samplerate;
|
||||
}
|
||||
|
||||
// check whether samplerates are one of the expected values
|
||||
uint32_t map_to_valid_samplerate(uint32_t measured) {
|
||||
const uint32_t valid_rates[] = {8000, 22050, 44100, 48000};
|
||||
const uint32_t num_rates = sizeof(valid_rates) / sizeof(valid_rates[0]);
|
||||
uint32_t closest_rate = valid_rates[0];
|
||||
int min_diff = abs((int)measured - (int)valid_rates[0]);
|
||||
|
||||
for (int i = 1; i < num_rates; i++) {
|
||||
int diff = abs((int)measured - (int)valid_rates[i]);
|
||||
if (diff < min_diff) {
|
||||
min_diff = diff;
|
||||
closest_rate = valid_rates[i];
|
||||
}
|
||||
}
|
||||
|
||||
// check tolerance
|
||||
if (min_diff <= (closest_rate * TOLERANCE_PERCENT / 100)) {
|
||||
return closest_rate;
|
||||
}
|
||||
return 0; // Invalid samplerates
|
||||
}
|
||||
|
||||
// task to monitor the samplerates
|
||||
void samplerate_monitor_task(void *pvParameters) {
|
||||
init_lrck_monitor();
|
||||
|
||||
while (1) {
|
||||
uint32_t new_samplerate = map_to_valid_samplerate(measure_samplerate());
|
||||
|
||||
if (new_samplerate != 0 && new_samplerate != current_samplerate) {
|
||||
log_w("Samplerate changed to %u Hz", new_samplerate);
|
||||
resampler.setInputSamplerate(new_samplerate);
|
||||
// response to samplerates change
|
||||
if (new_samplerate == 48000) {
|
||||
log_w("Activating resampling to 44100 Hz");
|
||||
// activate resampling logic here
|
||||
} else if (new_samplerate == 44100) {
|
||||
log_w("No resampling needed");
|
||||
// deactivate Resampling, direct transmission
|
||||
} else {
|
||||
log_w("Unsupported samplerate: %u Hz", new_samplerate);
|
||||
// treat 8000 or 22050 Hz, if necessary
|
||||
}
|
||||
current_samplerate = new_samplerate;
|
||||
}
|
||||
|
||||
vTaskDelay(500 / portTICK_PERIOD_MS); // Prüfe alle 500 ms
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------------------
|
||||
void i2s_install(){
|
||||
|
||||
i2s_chan_cfg.id = (i2s_port_t)i2s_num; // I2S_NUM_AUTO, I2S_NUM_0, I2S_NUM_1
|
||||
i2s_chan_cfg.role = I2S_ROLE_SLAVE; // I2S controller slave role, bclk and lrc signal will be set to input
|
||||
i2s_chan_cfg.dma_desc_num = 8; // number of DMA buffer
|
||||
i2s_chan_cfg.dma_frame_num = 512; // I2S frame number in one DMA buffer.
|
||||
i2s_chan_cfg.auto_clear = true; // i2s will always send zero automatically if no data to send
|
||||
i2s_new_channel(&i2s_chan_cfg, NULL, &i2s_rx_handle);
|
||||
|
||||
i2s_std_cfg.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO); // Set to enable bit shift in Philips mode
|
||||
i2s_std_cfg.gpio_cfg.bclk = (gpio_num_t)RX_I2S_BCLK; // BCLK Assignment
|
||||
i2s_std_cfg.gpio_cfg.din = (gpio_num_t)RX_I2S_DIN; // DIN Assignment
|
||||
i2s_std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; //
|
||||
i2s_std_cfg.gpio_cfg.mclk = I2S_GPIO_UNUSED; //
|
||||
i2s_std_cfg.gpio_cfg.ws = (gpio_num_t)RX_I2S_LRC; // LRC Assignment
|
||||
i2s_std_cfg.gpio_cfg.invert_flags.mclk_inv = false;
|
||||
i2s_std_cfg.gpio_cfg.invert_flags.bclk_inv = false;
|
||||
i2s_std_cfg.gpio_cfg.invert_flags.ws_inv = false;
|
||||
i2s_std_cfg.clk_cfg.sample_rate_hz = 44800;
|
||||
i2s_std_cfg.clk_cfg.clk_src = I2S_CLK_SRC_DEFAULT; // Select PLL_F160M as the default source clock
|
||||
i2s_std_cfg.clk_cfg.mclk_multiple = I2S_MCLK_MULTIPLE_128; //
|
||||
i2s_channel_init_std_mode(i2s_rx_handle, &i2s_std_cfg);
|
||||
i2s_channel_enable(i2s_rx_handle);
|
||||
}
|
||||
|
||||
//---------------------------------------------CallBacks--------------------------------------------------------------------
|
||||
|
||||
int32_t get_data(uint8_t *data, int32_t bytes) {
|
||||
return resampler.getData(data, bytes); // Holt exakt die benötigten Daten
|
||||
}
|
||||
|
||||
// gets called when button on bluetooth speaker is pressed
|
||||
void button_handler(uint8_t id, bool isReleased){
|
||||
if (isReleased) {
|
||||
Serial.print("button id ");
|
||||
Serial.print(id);
|
||||
Serial.println(" released");
|
||||
}
|
||||
}
|
||||
//---------------------------------------------SETUP--------------------------------------------------------------------
|
||||
void setup(){
|
||||
Serial.begin(115200);
|
||||
i2s_install();
|
||||
a2dp_source.set_data_callback(get_data);
|
||||
a2dp_source.set_avrc_passthru_command_callback(button_handler);
|
||||
a2dp_source.start(BT_SINK_NAME);
|
||||
resampler.setChannelHandle(i2s_rx_handle);
|
||||
xTaskCreate(samplerate_monitor_task, "samplerate_monitor", 2048, NULL, 5, NULL);
|
||||
}
|
||||
//----------------------------------------------LOOP--------------------------------------------------------------------
|
||||
void loop() {
|
||||
vTaskDelay(1);
|
||||
resampler.loopResample();
|
||||
}
|
||||
Reference in New Issue
Block a user