Add pre-packaged dependency libraries (ESP32-audioI2S and PNGdec)

This commit is contained in:
drjones
2026-07-01 19:18:01 -07:00
parent e207c9a5b7
commit 7606b9d5df
146 changed files with 106590 additions and 0 deletions

View File

@@ -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;
}
};

View File

@@ -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.
![image](https://github.com/user-attachments/assets/9e2d8066-f41e-41eb-9db5-e7b7a6e554e8)
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.
![image](https://github.com/user-attachments/assets/ac17cfa3-e473-4750-94ce-ee218827b3c3)
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.
![image](https://github.com/user-attachments/assets/0009dd9d-96b2-48b7-a6cc-bfc45dbc94d0)
Test circuit: the audioI2S library is running on the left, the BT transmitter on the right

View File

@@ -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();
}