Updated dependencies.

This commit is contained in:
Joe Kearney 2026-02-07 16:06:20 -06:00
parent d86c494d45
commit 58748fcef1
101 changed files with 5845 additions and 2391 deletions

View file

@ -1 +1 @@
c8ac1998e9af863bc41b57e592f88d1a5791a0f891485122336ddabbf7a65033
a19dba40c076e59baaf6383041b6feeb25135dbc5a5b8bcc6b497d8d947095c6

View file

@ -1,13 +1,20 @@
set(srcs
"audio_player.cpp"
"audio_mixer.cpp"
)
set(includes
"include"
)
set(requires "")
set(requires)
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.3")
list(APPEND requires esp_driver_i2s esp_ringbuf)
else()
list(APPEND requires driver)
endif()
if(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
list(APPEND srcs "audio_mp3.cpp")
@ -21,7 +28,6 @@ if(CONFIG_AUDIO_PLAYER_ENABLE_WAV)
endif()
idf_component_register(SRCS "${srcs}"
REQUIRES "${requires}"
INCLUDE_DIRS "${includes}"
REQUIRES driver
REQUIRES "${requires}"
)

View file

@ -7,6 +7,7 @@
* MP3 decoding (via libhelix-mp3)
* Wav/wave file decoding
* Audio mixing (multiple concurrent streams)
## Who is this for?
@ -49,6 +50,40 @@ For MP3 support you'll need the [esp-libhelix-mp3](https://github.com/chmorgan/e
Unity tests are implemented in the [test/](../test) folder.
## Audio Mixer
The Audio Mixer allows for concurrent playback of multiple audio streams. It supports two types of streams:
* **Decoder Streams**: For playing MP3 or WAV files. Each stream runs its own decoding task.
* **Raw PCM Streams**: For writing raw PCM data directly to the mixer.
### Basic Mixer Usage
1. Initialize the mixer with output format and I2S write functions.
2. Create one or more streams using `audio_stream_new()`.
3. Start playback on the streams.
```c
audio_mixer_config_t mixer_cfg = {
.write_fn = bsp_i2s_write,
.clk_set_fn = bsp_i2s_reconfig_clk,
.i2s_format = {
.sample_rate = 44100,
.bits_per_sample = 16,
.channels = 2
},
// ...
};
audio_mixer_init(&mixer_cfg);
audio_stream_config_t stream_cfg = DEFAULT_AUDIO_STREAM_CONFIG("bgm");
audio_stream_handle_t bgm_stream = audio_stream_new(&stream_cfg);
FILE *f = fopen("/sdcard/music.mp3", "rb");
audio_stream_play(bgm_stream, f);
```
## States
```mermaid

View file

@ -0,0 +1,35 @@
#pragma once
#include "esp_err.h"
#include "include/audio_player.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Opaque handle for a player instance.
* Used for multi-instance control in mixer
*/
typedef void* audio_instance_handle_t;
#define CHECK_INSTANCE(i) \
ESP_RETURN_ON_FALSE(i != NULL, ESP_ERR_INVALID_ARG, "audio_instance", "instance is NULL")
const char* event_to_string(audio_player_callback_event_t event);
audio_player_callback_event_t state_to_event(audio_player_state_t state);
audio_player_state_t audio_instance_get_state(audio_instance_handle_t h);
esp_err_t audio_instance_callback_register(audio_instance_handle_t h, audio_player_cb_t call_back, void *user_ctx);
esp_err_t audio_instance_play(audio_instance_handle_t h, FILE *fp);
esp_err_t audio_instance_pause(audio_instance_handle_t h);
esp_err_t audio_instance_resume(audio_instance_handle_t h);
esp_err_t audio_instance_stop(audio_instance_handle_t h);
esp_err_t audio_instance_new(audio_instance_handle_t *h, audio_player_config_t *config);
esp_err_t audio_instance_delete(audio_instance_handle_t h);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,527 @@
/**
* @file audio_mixer.cpp
*/
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/queue.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/ringbuf.h"
#include "freertos/semphr.h"
#include "esp_check.h"
#include "esp_log.h"
#include "audio_mixer.h"
#include "audio_player.h"
#include "audio_instance.h"
#include "audio_stream.h"
static const char *TAG = "audio_mixer";
static TaskHandle_t s_mixer_task = NULL;
static audio_mixer_config_t s_cfg = {};
static volatile bool s_running = false;
static audio_mixer_cb_t s_mixer_user_cb = NULL;
typedef struct audio_stream {
audio_stream_type_t type;
char name[16];
audio_instance_handle_t instance;
QueueHandle_t file_queue;
RingbufHandle_t pcm_rb;
audio_player_state_t state; // used only for RAW stream types.
SLIST_ENTRY(audio_stream) next;
} audio_stream_t;
SLIST_HEAD(audio_stream_list, audio_stream);
static audio_stream_list s_stream_list = SLIST_HEAD_INITIALIZER(s_stream_list);
static uint32_t s_stream_name_counter = 0; // counter for unique naming (monotonic)
static uint32_t s_active_streams = 0; // counter for stream counting
static SemaphoreHandle_t s_stream_mutex = NULL;
static int16_t sat_add16(int32_t a, int32_t b) {
int32_t s = a + b;
if (s > INT16_MAX) return INT16_MAX;
if (s < INT16_MIN) return INT16_MIN;
return (int16_t)s;
}
static void mixer_task(void *arg) {
const size_t frames = 512; // tune as needed
const size_t bytes = frames * s_cfg.i2s_format.channels * sizeof(int16_t);
int16_t *mix = static_cast<int16_t *>(heap_caps_malloc(bytes, MALLOC_CAP_8BIT));
ESP_ERROR_CHECK(mix == NULL);
while (s_running) {
memset(mix, 0, bytes);
audio_mixer_lock();
audio_stream_t *stream;
SLIST_FOREACH(stream, &s_stream_list, next) {
if (!stream->pcm_rb) continue;
size_t received_bytes = 0;
void *item = xRingbufferReceiveUpTo(stream->pcm_rb, &received_bytes, pdMS_TO_TICKS(5), bytes);
if (item && received_bytes > 0) {
int16_t *samples = static_cast<int16_t *>(item);
size_t count = received_bytes / sizeof(int16_t);
for (size_t k = 0; k < count; ++k) {
mix[k] = sat_add16(mix[k], samples[k]);
}
vRingbufferReturnItem(stream->pcm_rb, item);
} else if (item) {
vRingbufferReturnItem(stream->pcm_rb, item);
}
}
audio_mixer_unlock();
size_t written = 0;
if (s_cfg.write_fn) {
s_cfg.write_fn(mix, bytes, &written, portMAX_DELAY);
if (written != bytes) {
ESP_LOGW(TAG, "mixer short write %u/%u", (unsigned)written, (unsigned)bytes);
}
}
}
free(mix);
vTaskDelete(NULL);
}
IRAM_ATTR static esp_err_t mixer_stream_write(void *data, size_t size, size_t *bytes_written, uint32_t timeout, void *stream) {
audio_stream_t *s = static_cast<audio_stream_t *>(stream);
if (!s || !s->pcm_rb) {
if (bytes_written) *bytes_written = 0;
return ESP_ERR_INVALID_ARG;
}
/* send data to the stream's ring buffer */
BaseType_t res = xRingbufferSend(s->pcm_rb, data, size, timeout);
if (res == pdTRUE) {
if (bytes_written) *bytes_written = size;
} else {
if (bytes_written) *bytes_written = 0;
ESP_LOGW(TAG, "stream ringbuf full");
}
return ESP_OK;
}
static esp_err_t mixer_stream_clk_set_fn(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch) {
if (rate != s_cfg.i2s_format.sample_rate) {
ESP_LOGE(TAG, "stream sample rate mismatch: %lu Hz (mixer expects %u Hz)", rate, s_cfg.i2s_format.sample_rate);
return ESP_ERR_INVALID_ARG;
}
if (bits_cfg != s_cfg.i2s_format.bits_per_sample) {
ESP_LOGE(TAG, "stream bit depth mismatch: %lu bits (mixer expects %lu bits)", bits_cfg, s_cfg.i2s_format.bits_per_sample);
return ESP_ERR_INVALID_ARG;
}
if (ch != s_cfg.i2s_format.channels) {
ESP_LOGE(TAG, "stream channels mismatch: %u (mixer expects %lu)", ch, s_cfg.i2s_format.channels);
return ESP_ERR_INVALID_ARG;
}
return ESP_OK;
}
static void mixer_stream_event_handler(audio_player_cb_ctx_t *ctx) {
if (!ctx || !ctx->user_ctx) return;
audio_stream_t *s = static_cast<audio_stream_t *>(ctx->user_ctx);
// handle auto-queueing
if (ctx->audio_event == AUDIO_PLAYER_CALLBACK_EVENT_IDLE) {
if (s_stream_mutex) xSemaphoreTake(s_stream_mutex, portMAX_DELAY);
// Check if there is anything in the queue to play next
FILE *next_fp = NULL;
if (xQueueReceive(s->file_queue, &next_fp, 0) == pdTRUE) {
ESP_LOGD(TAG, "stream '%s' auto-advancing queue", s->name);
audio_instance_play(s->instance, next_fp);
}
audio_mixer_unlock();
}
// service callback
if (s_mixer_user_cb) {
s_mixer_user_cb(ctx);
}
}
static void mixer_free_stream_resources(audio_stream_t *s) {
if (s->instance) audio_instance_delete(s->instance);
if (s->pcm_rb) vRingbufferDelete(s->pcm_rb);
if (s->file_queue) {
FILE *fp = NULL;
while(xQueueReceive(s->file_queue, &fp, 0) == pdTRUE) {
if (fp) fclose(fp);
}
vQueueDelete(s->file_queue);
}
free(s);
}
/////////////////////////////
inline uint8_t audio_mixer_stream_count() {
return s_active_streams;
}
inline void audio_mixer_lock() {
if (s_stream_mutex) xSemaphoreTake(s_stream_mutex, portMAX_DELAY);
}
inline void audio_mixer_unlock() {
if (s_stream_mutex) xSemaphoreGive(s_stream_mutex);
}
void audio_mixer_add_stream(audio_stream_handle_t h) {
audio_mixer_lock();
SLIST_INSERT_HEAD(&s_stream_list, static_cast<audio_stream_t*>(h), next);
s_active_streams++;
audio_mixer_unlock();
}
void audio_mixer_remove_stream(audio_stream_handle_t h) {
audio_mixer_lock();
SLIST_REMOVE(&s_stream_list, static_cast<audio_stream_t*>(h), audio_stream, next);
if (s_active_streams > 0) s_active_streams--;
audio_mixer_unlock();
}
void audio_mixer_get_output_format(uint32_t *sample_rate, uint32_t *bits_per_sample, uint32_t *channels) {
if (sample_rate) *sample_rate = s_cfg.i2s_format.sample_rate;
if (bits_per_sample) *bits_per_sample = s_cfg.i2s_format.bits_per_sample;
if (channels) *channels = s_cfg.i2s_format.channels;
}
void audio_mixer_callback_register(audio_mixer_cb_t cb) {
s_mixer_user_cb = cb;
}
esp_err_t audio_mixer_init(audio_mixer_config_t *cfg) {
if (s_running) return ESP_OK;
ESP_RETURN_ON_FALSE(cfg && cfg->write_fn && cfg->clk_set_fn, ESP_ERR_INVALID_ARG, TAG, "invalid mixer config");
s_cfg = *cfg;
i2s_slot_mode_t channel_setting = (s_cfg.i2s_format.channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
ESP_RETURN_ON_ERROR(s_cfg.clk_set_fn(s_cfg.i2s_format.sample_rate, s_cfg.i2s_format.bits_per_sample, channel_setting), TAG, "clk set failed");
s_running = true;
if (!s_stream_mutex) s_stream_mutex = xSemaphoreCreateMutex();
SLIST_INIT(&s_stream_list);
BaseType_t ok = xTaskCreatePinnedToCore(mixer_task, "audio_mixer", 4096, NULL, s_cfg.priority, &s_mixer_task, s_cfg.coreID);
ESP_RETURN_ON_FALSE(ok == pdPASS, ESP_FAIL, TAG, "failed to start mixer");
ESP_LOGD(TAG, "mixer started");
return ESP_OK;
}
bool audio_mixer_is_initialized() {
return s_mixer_task != NULL;
}
void audio_mixer_deinit() {
if (!s_running) return;
// Task will exit on next loop; no join primitive in FreeRTOS here.
s_running = false;
s_mixer_task = NULL;
// Clean up any remaining channels (safe teardown)
audio_mixer_lock();
while (!SLIST_EMPTY(&s_stream_list)) {
audio_stream_t *it = SLIST_FIRST(&s_stream_list);
SLIST_REMOVE_HEAD(&s_stream_list, next);
mixer_free_stream_resources(it);
}
s_active_streams = 0;
audio_mixer_unlock();
}
/* ================= Stream (mixer channel) API ================= */
static void dispatch_callback(audio_stream_t *s, audio_player_callback_event_t event) {
ESP_LOGD(TAG, "event '%s'", event_to_string(event));
#if CONFIG_IDF_TARGET_ARCH_XTENSA
if (esp_ptr_executable(reinterpret_cast<void*>(s_mixer_user_cb))) {
#else
if (reinterpret_cast<void*>(s_mixer_user_cb)) {
#endif
audio_player_cb_ctx_t ctx = {
.audio_event = event,
.user_ctx = s,
};
s_mixer_user_cb(&ctx);
}
}
static void stream_purge_ringbuf(audio_stream_t *s) {
if (!s || !s->pcm_rb) return;
size_t item_size;
void *item;
while ((item = xRingbufferReceive(s->pcm_rb, &item_size, 0)) != NULL) {
vRingbufferReturnItem(s->pcm_rb, item);
}
}
esp_err_t audio_stream_raw_send_event(audio_stream_handle_t h, audio_player_callback_event_t event) {
audio_stream_t *s = h;
CHECK_STREAM(s);
if (s->type != AUDIO_STREAM_TYPE_RAW) return ESP_ERR_NOT_SUPPORTED;
// NOTE: essentially made event_to_state()
audio_player_state_t new_state = AUDIO_PLAYER_STATE_IDLE;
switch (event) {
case AUDIO_PLAYER_CALLBACK_EVENT_IDLE:
new_state = AUDIO_PLAYER_STATE_IDLE;
break;
case AUDIO_PLAYER_CALLBACK_EVENT_PLAYING:
case AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT:
new_state = AUDIO_PLAYER_STATE_PLAYING;
break;
case AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN:
new_state = AUDIO_PLAYER_STATE_SHUTDOWN;
break;
default:
new_state = AUDIO_PLAYER_STATE_IDLE;
break;
}
if(s->state != new_state) {
s->state = new_state;
dispatch_callback(s, event);
}
return ESP_OK;
}
audio_player_state_t audio_stream_get_state(audio_stream_handle_t h) {
audio_stream_t *s = h;
if (!s) return AUDIO_PLAYER_STATE_IDLE;
/* DECODER stream? defer to the instance state */
if (s->type == AUDIO_STREAM_TYPE_DECODER) {
return audio_instance_get_state(s->instance);
}
/* RAW stream? check if ringbuf has data */
if (s->type == AUDIO_STREAM_TYPE_RAW) {
// TODO: determine if checking ringbuf is valuable vs. having a stream emit its own state
// using the method audio_stream_raw_send_event().
// if (!s->pcm_rb) return AUDIO_PLAYER_STATE_IDLE;
//
// // peek for any bytes
// UBaseType_t items_waiting = 0;
// vRingbufferGetInfo(s->pcm_rb, NULL, NULL, NULL, NULL, &items_waiting);
//
// if (items_waiting > 0)
// return AUDIO_PLAYER_STATE_PLAYING;
return s->state;
}
return AUDIO_PLAYER_STATE_IDLE;
}
audio_stream_type_t audio_stream_get_type(audio_stream_handle_t h) {
if (!h) return AUDIO_STREAM_TYPE_UNKNOWN;
return h->type;
}
esp_err_t audio_stream_play(audio_stream_handle_t h, FILE *fp) {
audio_stream_t *s = h;
CHECK_STREAM(s);
if (s->type != AUDIO_STREAM_TYPE_DECODER) {
ESP_LOGE(TAG, "stream '%s' is not a decoder stream", s->name);
return ESP_ERR_NOT_SUPPORTED;
}
// stop current playback?
if (audio_instance_get_state(s->instance) == AUDIO_PLAYER_STATE_PLAYING)
audio_stream_stop(s);
return audio_instance_play(s->instance, fp);
}
esp_err_t audio_stream_queue(audio_stream_handle_t h, FILE *fp, bool play_now) {
if (play_now) {
return audio_stream_play(h, fp);
}
audio_stream_t *s = h;
CHECK_STREAM(s);
if (s->type != AUDIO_STREAM_TYPE_DECODER) {
ESP_LOGE(TAG, "stream '%s' is not a decoder stream", s->name);
return ESP_ERR_NOT_SUPPORTED;
}
audio_mixer_lock();
// add to queue
if (xQueueSend(s->file_queue, &fp, 0) != pdTRUE) {
ESP_LOGE(TAG, "stream '%s' queue full", s->name);
fclose(fp); // Take ownership and close if we can't queue
audio_mixer_unlock();
return ESP_FAIL;
}
// if stream is IDLE, we need to kickstart it
if (audio_instance_get_state(s->instance) == AUDIO_PLAYER_STATE_IDLE) {
FILE *next_fp = NULL;
// pop the one we just pushed (or the one at head)
if (xQueueReceive(s->file_queue, &next_fp, 0) == pdTRUE) {
audio_instance_play(s->instance, next_fp);
}
}
audio_mixer_unlock();
return ESP_OK;
}
esp_err_t audio_stream_stop(audio_stream_handle_t h) {
audio_stream_t *s = h;
CHECK_STREAM(s);
esp_err_t err = ESP_OK;
if (s->type == AUDIO_STREAM_TYPE_DECODER) {
// clear any pending queue items
FILE *pending = NULL;
while (xQueueReceive(s->file_queue, &pending, 0) == pdTRUE) {
if (pending) fclose(pending);
}
err = audio_instance_stop(s->instance);
}
stream_purge_ringbuf(s);
return err;
}
esp_err_t audio_stream_pause(audio_stream_handle_t h) {
audio_stream_t *s = h;
CHECK_STREAM(s);
if (s->type != AUDIO_STREAM_TYPE_DECODER) return ESP_ERR_NOT_SUPPORTED;
return audio_instance_pause(s->instance);
}
esp_err_t audio_stream_resume(audio_stream_handle_t h) {
audio_stream_t *s = h;
CHECK_STREAM(s);
if (s->type != AUDIO_STREAM_TYPE_DECODER) return ESP_ERR_NOT_SUPPORTED;
return audio_instance_resume(s->instance);
}
esp_err_t audio_stream_write_pcm(audio_stream_handle_t h, void *data, size_t size, uint32_t timeout_ms) {
audio_stream_t *s = h;
CHECK_STREAM(s);
if (s->type != AUDIO_STREAM_TYPE_RAW) {
ESP_LOGE(TAG, "stream '%s' is not a raw stream", s->name);
return ESP_ERR_NOT_SUPPORTED;
}
if (!s->pcm_rb) return ESP_ERR_INVALID_STATE;
// Send data to the ring buffer (BYTEBUF type)
BaseType_t res = xRingbufferSend(s->pcm_rb, data, size, pdMS_TO_TICKS(timeout_ms));
if (res != pdTRUE) {
ESP_LOGW(TAG, "stream '%s' overflow", s->name);
return ESP_FAIL;
}
return ESP_OK;
}
audio_stream_handle_t audio_stream_new(audio_stream_config_t *cfg) {
ESP_RETURN_ON_FALSE(cfg, NULL, TAG, "null config");
audio_stream_t *stream = static_cast<audio_stream_t *>(calloc(1, sizeof(audio_stream_t)));
stream->type = cfg->type;
/* use provided name? */
if (cfg->name[0] != '\0') {
strncpy(stream->name, cfg->name, sizeof(stream->name) - 1);
stream->name[sizeof(stream->name) - 1] = 0;
}
/* otherwise, generate a unique monotonic name */
else {
snprintf(stream->name, sizeof(stream->name), "stream_%lu", static_cast<unsigned long>(s_stream_name_counter++));
}
/* DECODER type stream? create a player instance and queue */
if (cfg->type == AUDIO_STREAM_TYPE_DECODER) {
// new player instance
audio_player_config_t instance_cfg;
instance_cfg.mute_fn = NULL;
instance_cfg.clk_set_fn = mixer_stream_clk_set_fn;
instance_cfg.coreID = cfg->coreID;
instance_cfg.priority = cfg->priority;
instance_cfg.force_stereo = false;
instance_cfg.write_fn2 = mixer_stream_write;
instance_cfg.write_ctx = stream;
audio_instance_handle_t h = NULL;
esp_err_t err = audio_instance_new(&h, &instance_cfg);
if (err != ESP_OK) {
free(stream);
return NULL;
}
stream->instance = h;
// create file queue & attach event handler
stream->file_queue = xQueueCreate(4, sizeof(FILE*));
audio_instance_callback_register(stream->instance, mixer_stream_event_handler, stream);
}
/* always create a ringbuffer */
stream->pcm_rb = xRingbufferCreate(16 * 1024, RINGBUF_TYPE_BYTEBUF);
if (!stream->pcm_rb || (cfg->type == AUDIO_STREAM_TYPE_DECODER && !stream->file_queue)) {
if (stream->file_queue) vQueueDelete(stream->file_queue);
if (stream->pcm_rb) vRingbufferDelete(stream->pcm_rb);
if (stream->instance) audio_instance_delete(stream->instance);
free(stream);
return NULL;
}
/* add to stream tracking */
audio_mixer_add_stream(stream);
ESP_LOGI(TAG, "Created stream '%s' (active: %u)", stream->name, audio_mixer_stream_count());
return stream;
}
esp_err_t audio_stream_delete(audio_stream_handle_t h) {
audio_stream_t *s = h;
CHECK_STREAM(s);
/* remove from stream tracking */
audio_mixer_remove_stream(s);
/* cleanup stream */
mixer_free_stream_resources(s);
ESP_LOGI(TAG, "Deleted stream '%s' (active: %u)", s->name, audio_mixer_stream_count());
return ESP_OK;
}

View file

@ -35,6 +35,7 @@
#include "sdkconfig.h"
#include "audio_player.h"
#include "audio_instance.h"
#include "audio_wav.h"
#include "audio_mp3.h"
@ -94,16 +95,18 @@ typedef struct audio_instance {
HMP3Decoder mp3_decoder;
mp3_instance mp3_data;
#endif
format i2s_format; // last configured i2s format
} audio_instance_t;
static audio_instance_t instance;
static audio_instance_t *g_instance = NULL; // when non-null, in legacy non-mixer mode
audio_player_state_t audio_player_get_state() {
return instance.state;
audio_player_state_t audio_instance_get_state(audio_instance_handle_t h) {
audio_instance_t *i = static_cast<audio_instance_t *>(h);
return i ? i->state : AUDIO_PLAYER_STATE_IDLE;
}
esp_err_t audio_player_callback_register(audio_player_cb_t call_back, void *user_ctx)
{
esp_err_t audio_instance_callback_register(audio_instance_handle_t h, audio_player_cb_t call_back, void *user_ctx) {
#if CONFIG_IDF_TARGET_ARCH_XTENSA
ESP_RETURN_ON_FALSE(esp_ptr_executable(reinterpret_cast<void*>(call_back)), ESP_ERR_INVALID_ARG,
TAG, "Not a valid call back");
@ -111,15 +114,14 @@ esp_err_t audio_player_callback_register(audio_player_cb_t call_back, void *user
ESP_RETURN_ON_FALSE(reinterpret_cast<void*>(call_back), ESP_ERR_INVALID_ARG,
TAG, "Not a valid call back");
#endif
instance.s_audio_cb = call_back;
instance.audio_cb_usrt_ctx = user_ctx;
audio_instance_t *i = static_cast<audio_instance_t *>(h);
CHECK_INSTANCE(i);
i->s_audio_cb = call_back;
i->audio_cb_usrt_ctx = user_ctx;
return ESP_OK;
}
// This function is used in some optional logging functions so we don't want to
// have a cppcheck warning here
// cppcheck-suppress unusedFunction
const char* event_to_string(audio_player_callback_event_t event) {
switch(event) {
case AUDIO_PLAYER_CALLBACK_EVENT_IDLE:
@ -141,7 +143,7 @@ const char* event_to_string(audio_player_callback_event_t event) {
return "unknown event";
}
static audio_player_callback_event_t state_to_event(audio_player_state_t state) {
audio_player_callback_event_t state_to_event(audio_player_state_t state) {
audio_player_callback_event_t event = AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN;
switch(state) {
@ -186,15 +188,15 @@ static void set_state(audio_instance_t *i, audio_player_state_t new_state) {
}
}
static void audio_instance_init(audio_instance_t &i) {
i.event_queue = NULL;
i.s_audio_cb = NULL;
i.audio_cb_usrt_ctx = NULL;
i.state = AUDIO_PLAYER_STATE_IDLE;
static void audio_instance_init(audio_instance_t *i) {
i->event_queue = NULL;
i->s_audio_cb = NULL;
i->audio_cb_usrt_ctx = NULL;
i->state = AUDIO_PLAYER_STATE_IDLE;
memset(&i->i2s_format, 0, sizeof(i->i2s_format));
}
static esp_err_t mono_to_stereo(uint32_t output_bits_per_sample, decode_data &adata)
{
static esp_err_t mono_to_stereo(uint32_t output_bits_per_sample, decode_data &adata) {
size_t data = adata.frame_count * (output_bits_per_sample / BITS_PER_BYTE);
data *= 2;
@ -234,13 +236,9 @@ static esp_err_t mono_to_stereo(uint32_t output_bits_per_sample, decode_data &ad
return ESP_OK;
}
static esp_err_t aplay_file(audio_instance_t *i, FILE *fp)
{
static esp_err_t aplay_file(audio_instance_t *i, FILE *fp) {
LOGI_1("start to decode");
format i2s_format;
memset(&i2s_format, 0, sizeof(i2s_format));
esp_err_t ret = ESP_OK;
audio_player_event_t audio_event = { .type = AUDIO_PLAYER_REQUEST_NONE, .fp = NULL };
@ -348,9 +346,9 @@ static esp_err_t aplay_file(audio_instance_t *i, FILE *fp)
// break out and exit if we aren't supposed to continue decoding
if(decode_status == DECODE_STATUS_CONTINUE)
{
// if mono, convert to stereo as es8311 requires stereo input
// if mono and force_stereo set, convert to stereo as es8311 requires stereo input
// even though it is mono output
if(i->output.fmt.channels == 1) {
if(i->output.fmt.channels == 1 && i->config.force_stereo) {
LOGI_3("c == 1, mono -> stereo");
ret = mono_to_stereo(i->output.fmt.bits_per_sample, i->output);
if(ret != ESP_OK) {
@ -359,17 +357,17 @@ static esp_err_t aplay_file(audio_instance_t *i, FILE *fp)
}
/* Configure I2S clock if the output format changed */
if ((i2s_format.sample_rate != i->output.fmt.sample_rate) ||
(i2s_format.channels != i->output.fmt.channels) ||
(i2s_format.bits_per_sample != i->output.fmt.bits_per_sample)) {
i2s_format = i->output.fmt;
LOGI_1("format change: sr=%d, bit=%d, ch=%d",
i2s_format.sample_rate,
i2s_format.bits_per_sample,
i2s_format.channels);
i2s_slot_mode_t channel_setting = (i2s_format.channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
ret = i->config.clk_set_fn(i2s_format.sample_rate,
i2s_format.bits_per_sample,
if ((i->i2s_format.sample_rate != i->output.fmt.sample_rate) ||
(i->i2s_format.channels != i->output.fmt.channels) ||
(i->i2s_format.bits_per_sample != i->output.fmt.bits_per_sample)) {
i->i2s_format = i->output.fmt;
LOGI_1("format change: sr=%d, bit=%lu, ch=%lu",
i->i2s_format.sample_rate,
i->i2s_format.bits_per_sample,
i->i2s_format.channels);
i2s_slot_mode_t channel_setting = (i->i2s_format.channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
ret = i->config.clk_set_fn(i->i2s_format.sample_rate,
i->i2s_format.bits_per_sample,
channel_setting);
ESP_GOTO_ON_ERROR(ret, clean_up, TAG, "i2s_set_clk");
}
@ -380,17 +378,22 @@ static esp_err_t aplay_file(audio_instance_t *i, FILE *fp)
* audio decoding to occur while the previous set of samples is finishing playback, in order
* to ensure playback without interruption.
*/
size_t i2s_bytes_written = 0;
size_t bytes_to_write = i->output.frame_count * i->output.fmt.channels * (i2s_format.bits_per_sample / 8);
size_t bytes_written = 0;
size_t bytes_to_write = i->output.frame_count * i->output.fmt.channels * (i->i2s_format.bits_per_sample / 8);
LOGI_2("c %d, bps %d, bytes %d, frame_count %d",
i->output.fmt.channels,
i2s_format.bits_per_sample,
bytes_to_write,
i->output.frame_count);
i->config.write_fn(i->output.samples, bytes_to_write, &i2s_bytes_written, portMAX_DELAY);
if(bytes_to_write != i2s_bytes_written) {
ESP_LOGE(TAG, "to write %d != written %d", bytes_to_write, i2s_bytes_written);
// NOTE: to aid transition in api, using write_fn2 based on write_ctx assignment
if (i->config.write_ctx)
i->config.write_fn2(i->output.samples, bytes_to_write, &bytes_written, portMAX_DELAY, i->config.write_ctx);
else
i->config.write_fn(i->output.samples, bytes_to_write, &bytes_written, portMAX_DELAY);
if(bytes_to_write != bytes_written) {
ESP_LOGE(TAG, "to write %d != written %d", bytes_to_write, bytes_written);
}
} else if(decode_status == DECODE_STATUS_NO_DATA_CONTINUE)
{
@ -405,8 +408,7 @@ clean_up:
return ret;
}
static void audio_task(void *pvParam)
{
static void audio_task(void *pvParam) {
audio_instance_t *i = static_cast<audio_instance_t*>(pvParam);
audio_player_event_t audio_event;
@ -451,13 +453,13 @@ static void audio_task(void *pvParam)
}
}
i->config.mute_fn(AUDIO_PLAYER_UNMUTE);
if (i->config.mute_fn) i->config.mute_fn(AUDIO_PLAYER_UNMUTE);
esp_err_t ret_val = aplay_file(i, audio_event.fp);
if(ret_val != ESP_OK)
{
ESP_LOGE(TAG, "aplay_file() %d", ret_val);
}
i->config.mute_fn(AUDIO_PLAYER_MUTE);
if (i->config.mute_fn) i->config.mute_fn(AUDIO_PLAYER_MUTE);
if(audio_event.fp) fclose(audio_event.fp);
}
@ -476,128 +478,155 @@ static esp_err_t audio_send_event(audio_instance_t *i, audio_player_event_t even
return ESP_OK;
}
esp_err_t audio_player_play(FILE *fp)
{
/* ================= New multi-instance API ================= */
esp_err_t audio_instance_play(audio_instance_handle_t h, FILE *fp) {
audio_instance_t *i = static_cast<audio_instance_t *>(h);
CHECK_INSTANCE(i);
LOGI_1("%s", __FUNCTION__);
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_PLAY, .fp = fp };
return audio_send_event(&instance, event);
return audio_send_event(i, event);
}
esp_err_t audio_player_pause(void)
{
esp_err_t audio_instance_pause(audio_instance_handle_t h) {
audio_instance_t *i = static_cast<audio_instance_t *>(h);
CHECK_INSTANCE(i);
LOGI_1("%s", __FUNCTION__);
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_PAUSE, .fp = NULL };
return audio_send_event(&instance, event);
return audio_send_event(i, event);
}
esp_err_t audio_player_resume(void)
{
esp_err_t audio_instance_resume(audio_instance_handle_t h) {
audio_instance_t *i = static_cast<audio_instance_t *>(h);
CHECK_INSTANCE(i);
LOGI_1("%s", __FUNCTION__);
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_RESUME, .fp = NULL };
return audio_send_event(&instance, event);
return audio_send_event(i, event);
}
esp_err_t audio_player_stop(void)
{
esp_err_t audio_instance_stop(audio_instance_handle_t h) {
audio_instance_t *i = static_cast<audio_instance_t *>(h);
CHECK_INSTANCE(i);
LOGI_1("%s", __FUNCTION__);
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_STOP, .fp = NULL };
return audio_send_event(&instance, event);
return audio_send_event(i, event);
}
/**
* Can only shut down the playback thread if the thread is not presently playing audio.
* Call audio_player_stop()
*/
static esp_err_t _internal_audio_player_shutdown_thread(void)
{
static esp_err_t _internal_audio_player_shutdown_thread(audio_instance_t *i) {
CHECK_INSTANCE(i);
LOGI_1("%s", __FUNCTION__);
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_SHUTDOWN_THREAD, .fp = NULL };
return audio_send_event(&instance, event);
return audio_send_event(i, event);
}
static void cleanup_memory(audio_instance_t &i)
{
static void cleanup_memory(audio_instance_t *i) {
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
if(i.mp3_decoder) MP3FreeDecoder(i.mp3_decoder);
if(i.mp3_data.data_buf) free(i.mp3_data.data_buf);
if(i->mp3_decoder) MP3FreeDecoder(i->mp3_decoder);
if(i->mp3_data.data_buf) free(i->mp3_data.data_buf);
#endif
if(i.output.samples) free(i.output.samples);
if(i->output.samples) free(i->output.samples);
vQueueDelete(i.event_queue);
vQueueDelete(i->event_queue);
}
esp_err_t audio_player_new(audio_player_config_t config)
{
esp_err_t audio_instance_new(audio_instance_handle_t *h, audio_player_config_t *config) {
BaseType_t task_val;
audio_instance_init(instance);
ESP_RETURN_ON_FALSE(h != NULL, ESP_ERR_INVALID_ARG, TAG, "handle pointer is NULL");
ESP_RETURN_ON_FALSE(*h == NULL, ESP_ERR_INVALID_ARG, TAG, "instance is not NULL");
ESP_RETURN_ON_FALSE(config, ESP_ERR_INVALID_ARG, TAG, "null config");
instance.config = config;
audio_instance_t *i = static_cast<audio_instance_t *>(calloc(1, sizeof(audio_instance_t)));
if (i == NULL) return ESP_ERR_NO_MEM;
audio_instance_init(i);
i->config = *config;
/* Audio control event queue */
instance.event_queue = xQueueCreate(4, sizeof(audio_player_event_t));
ESP_RETURN_ON_FALSE(NULL != instance.event_queue, -1, TAG, "xQueueCreate");
i->event_queue = xQueueCreate(4, sizeof(audio_player_event_t));
ESP_RETURN_ON_FALSE(NULL != i->event_queue, -1, TAG, "xQueueCreate");
/** See https://github.com/ultraembedded/libhelix-mp3/blob/0a0e0673f82bc6804e5a3ddb15fb6efdcde747cd/testwrap/main.c#L74 */
instance.output.samples_capacity = MAX_NCHAN * MAX_NGRAN * MAX_NSAMP;
instance.output.samples_capacity_max = instance.output.samples_capacity * 2;
instance.output.samples = static_cast<uint8_t*>(malloc(instance.output.samples_capacity_max));
LOGI_1("samples_capacity %d bytes", instance.output.samples_capacity_max);
i->output.samples_capacity = MAX_NCHAN * MAX_NGRAN * MAX_NSAMP;
i->output.samples_capacity_max = i->output.samples_capacity * 2;
i->output.samples = static_cast<uint8_t*>(malloc(i->output.samples_capacity_max));
LOGI_1("samples_capacity %d bytes", i->output.samples_capacity_max);
int ret = ESP_OK;
ESP_GOTO_ON_FALSE(NULL != instance.output.samples, ESP_ERR_NO_MEM, cleanup,
ESP_GOTO_ON_FALSE(NULL != i->output.samples, ESP_ERR_NO_MEM, cleanup,
TAG, "Failed allocate output buffer");
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
instance.mp3_data.data_buf_size = MAINBUF_SIZE * 3;
instance.mp3_data.data_buf = static_cast<uint8_t*>(malloc(instance.mp3_data.data_buf_size));
ESP_GOTO_ON_FALSE(NULL != instance.mp3_data.data_buf, ESP_ERR_NO_MEM, cleanup,
i->mp3_data.data_buf_size = MAINBUF_SIZE * 3;
i->mp3_data.data_buf = static_cast<uint8_t*>(malloc(i->mp3_data.data_buf_size));
ESP_GOTO_ON_FALSE(NULL != i->mp3_data.data_buf, ESP_ERR_NO_MEM, cleanup,
TAG, "Failed allocate mp3 data buffer");
instance.mp3_decoder = MP3InitDecoder();
ESP_GOTO_ON_FALSE(NULL != instance.mp3_decoder, ESP_ERR_NO_MEM, cleanup,
i->mp3_decoder = MP3InitDecoder();
ESP_GOTO_ON_FALSE(NULL != i->mp3_decoder, ESP_ERR_NO_MEM, cleanup,
TAG, "Failed create MP3 decoder");
#endif
instance.running = true;
memset(&i->i2s_format, 0, sizeof(i->i2s_format));
i->running = true;
task_val = xTaskCreatePinnedToCore(
(TaskFunction_t) audio_task,
"Audio Task",
4 * 1024,
&instance,
(UBaseType_t) instance.config.priority,
(TaskHandle_t * const) NULL,
(BaseType_t) instance.config.coreID);
i,
(UBaseType_t) i->config.priority,
(TaskHandle_t *) NULL,
(BaseType_t) i->config.coreID);
ESP_GOTO_ON_FALSE(pdPASS == task_val, ESP_ERR_NO_MEM, cleanup,
TAG, "Failed create audio task");
// start muted
instance.config.mute_fn(AUDIO_PLAYER_MUTE);
if (i->config.mute_fn)
i->config.mute_fn(AUDIO_PLAYER_MUTE);
*h = i;
return ret;
// At the moment when we run cppcheck there is a lack of esp-idf header files this
// means cppcheck doesn't know that ESP_GOTO_ON_FALSE() etc are making use of this label
// cppcheck-suppress unusedLabelConfiguration
cleanup:
cleanup_memory(instance);
cleanup_memory(i);
free(i);
i = NULL;
return ret;
}
esp_err_t audio_player_delete() {
esp_err_t audio_instance_delete(audio_instance_handle_t h) {
audio_instance_t *i = static_cast<audio_instance_t *>(h);
CHECK_INSTANCE(i);
const int MAX_RETRIES = 5;
int retries = MAX_RETRIES;
while(instance.running && retries) {
while(i->running && retries) {
// stop any playback and shutdown the thread
audio_player_stop();
_internal_audio_player_shutdown_thread();
audio_instance_stop(i);
_internal_audio_player_shutdown_thread(i);
vTaskDelay(pdMS_TO_TICKS(100));
retries--;
}
cleanup_memory(instance);
cleanup_memory(i);
free(i);
i = NULL;
// if we ran out of retries, return fail code
if(retries == 0) {
@ -606,3 +635,46 @@ esp_err_t audio_player_delete() {
return ESP_OK;
}
/* ================= Legacy API implemented via default instance ================= */
audio_player_state_t audio_player_get_state() {
return audio_instance_get_state(g_instance);
}
esp_err_t audio_player_callback_register(audio_player_cb_t call_back, void *user_ctx) {
return audio_instance_callback_register(g_instance, call_back, user_ctx);
}
esp_err_t audio_player_play(FILE *fp) {
return audio_instance_play(g_instance, fp);
}
esp_err_t audio_player_pause() {
return audio_instance_pause(g_instance);
}
esp_err_t audio_player_resume() {
return audio_instance_resume(g_instance);
}
esp_err_t audio_player_stop() {
return audio_instance_stop(g_instance);
}
esp_err_t audio_player_new(audio_player_config_t config) {
if (g_instance) return ESP_OK;
config.force_stereo = true; // preserve legacy behavior
audio_instance_handle_t h = NULL;
ESP_RETURN_ON_ERROR(audio_instance_new(&h, &config), TAG, "failed to create new audio instance");
g_instance = static_cast<audio_instance_t *>(h);
return ESP_OK;
}
esp_err_t audio_player_delete() {
if (g_instance) {
audio_instance_delete(g_instance);
g_instance = NULL;
}
return ESP_OK;
}

View file

@ -1,8 +1,7 @@
dependencies:
chmorgan/esp-libhelix-mp3:
version: '>=1.0.0,<2.0.0'
chmorgan/esp-libhelix-mp3: '>=1.0.0,<2.0.0'
idf:
version: '>=5.0'
description: Lightweight audio decoding component for esp processors
url: https://github.com/chmorgan/esp-audio-player
version: 1.0.7
version: 1.1.0

View file

@ -0,0 +1,118 @@
/**
* @file audio_mixer.h
* @brief Mixer interface for esp-audio-player. Provides a global mixer that accepts
* PCM from multiple sources via FreeRTOS ring buffers and writes mixed PCM to I2S.
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "audio_player.h"
#include "../audio_decode_types.h" // FIXME: leaks out
#include "audio_stream.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Configuration structure for the audio mixer
*/
typedef struct {
audio_player_mute_fn mute_fn; /**< Function to mute/unmute audio */
audio_reconfig_std_clock clk_set_fn; /**< Function to reconfigure I2S clock */
audio_player_write_fn write_fn; /**< Function to write PCM data to I2S */
UBaseType_t priority; /**< FreeRTOS task priority for the mixer task */
BaseType_t coreID; /**< ESP32 core ID for the mixer task */
format i2s_format; /**< Fixed output format for the mixer */
} audio_mixer_config_t;
/**
* @brief Mixer callback function type
*/
typedef audio_player_cb_t audio_mixer_cb_t;
/**
* @brief Get the number of active streams in the mixer
*
* @return Number of active streams
*/
uint8_t audio_mixer_stream_count();
/**
* @brief Lock the mixer's main mutex
*
* Call this before modifying stream state (busy flags, queues).
*/
void audio_mixer_lock();
/**
* @brief Unlock the mixer's main mutex
*/
void audio_mixer_unlock();
/**
* @brief Add a stream to the mixer's processing list
*
* This function is thread-safe.
*
* @param h Handle of the stream to add
*/
void audio_mixer_add_stream(audio_stream_handle_t h);
/**
* @brief Remove a stream from the mixer's processing list
*
* This function is thread-safe.
*
* @param h Handle of the stream to remove
*/
void audio_mixer_remove_stream(audio_stream_handle_t h);
/**
* @brief Query the current mixer output format
*
* Returns zeros if the mixer is not initialized.
*
* @param[out] sample_rate Pointer to store the sample rate
* @param[out] bits_per_sample Pointer to store the bits per sample
* @param[out] channels Pointer to store the number of channels
*/
void audio_mixer_get_output_format(uint32_t *sample_rate, uint32_t *bits_per_sample, uint32_t *channels);
/**
* @brief Register a global callback for mixer events
*
* @param cb Callback function to register
*/
void audio_mixer_callback_register(audio_mixer_cb_t cb);
/**
* @brief Check if the mixer is initialized
*
* @return true if initialized, false otherwise
*/
bool audio_mixer_is_initialized();
/**
* @brief Initialize the mixer and start the mixer task
*
* @param cfg Pointer to the mixer configuration structure
* @return
* - ESP_OK: Success
* - ESP_ERR_INVALID_ARG: Invalid configuration
* - Others: Fail
*/
esp_err_t audio_mixer_init(audio_mixer_config_t *cfg);
/**
* @brief Deinitialize the mixer and stop the mixer task
*/
void audio_mixer_deinit();
#ifdef __cplusplus
}
#endif

View file

@ -152,6 +152,7 @@ typedef enum {
typedef esp_err_t (*audio_player_mute_fn)(AUDIO_PLAYER_MUTE_SETTING setting);
typedef esp_err_t (*audio_reconfig_std_clock)(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch);
typedef esp_err_t (*audio_player_write_fn)(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms);
typedef esp_err_t (*audio_player_write_fn2)(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms, void *ctx);
typedef struct {
audio_player_mute_fn mute_fn;
@ -159,6 +160,10 @@ typedef struct {
audio_player_write_fn write_fn;
UBaseType_t priority; /*< FreeRTOS task priority */
BaseType_t coreID; /*< ESP32 core ID */
bool force_stereo; /*< upmix mono -> stereo */
audio_player_write_fn2 write_fn2;
void *write_ctx;
} audio_player_config_t;
/**

View file

@ -0,0 +1,188 @@
/**
* @file audio_stream.h
* @brief Stream API create/delete logical playback streams and control them.
* These streams own their decode task and submit PCM to the mixer.
*/
#pragma once
#include "audio_player.h"
#ifdef __cplusplus
extern "C" {
#endif
struct audio_stream;
/**
* @brief Audio stream handle
*/
typedef struct audio_stream* audio_stream_handle_t;
/**
* @brief Macro to check if a stream handle is valid
*/
#define CHECK_STREAM(s) \
ESP_RETURN_ON_FALSE(s != NULL, ESP_ERR_INVALID_ARG, "audio_stream", "stream is NULL")
/**
* @brief Audio stream types
*/
typedef enum {
AUDIO_STREAM_TYPE_UNKNOWN = 0, /**< Unknown stream type */
AUDIO_STREAM_TYPE_DECODER, /**< Stream that decodes audio (e.g., MP3, WAV) */
AUDIO_STREAM_TYPE_RAW /**< Stream that accepts raw PCM data */
} audio_stream_type_t;
/**
* @brief Configuration structure for an audio stream
*/
typedef struct {
audio_stream_type_t type; /**< Type of stream */
char name[16]; /**< Optional: Name of the stream (e.g. "sfx", "bgm"). Auto-generated if empty. */
UBaseType_t priority; /**< FreeRTOS task priority for the stream's decoder task (if applicable) */
BaseType_t coreID; /**< ESP32 core ID for the stream's decoder task (if applicable) */
} audio_stream_config_t;
/**
* @brief Default configuration for an audio decoder stream
*
* @param _name Name of the stream
*/
#define DEFAULT_AUDIO_STREAM_CONFIG(_name) { \
.type = AUDIO_STREAM_TYPE_DECODER, \
.name = _name, \
.priority = tskIDLE_PRIORITY + 1, \
.coreID = tskNO_AFFINITY \
}
/**
* @brief Get the current state of a stream
*
* @param h Handle of the stream
* @return Current audio_player_state_t of the stream
*/
audio_player_state_t audio_stream_get_state(audio_stream_handle_t h);
/**
* @brief Get the type of a stream
*
* @param h Handle of the stream
* @return audio_stream_type_t of the stream
*/
audio_stream_type_t audio_stream_get_type(audio_stream_handle_t h);
/**
* @brief Play an audio file on a stream
*
* Only supported for DECODER type streams.
*
* @param h Handle of the stream
* @param fp File pointer to the audio file
* @return
* - ESP_OK: Success
* - ESP_ERR_NOT_SUPPORTED: Stream is not a decoder stream
* - Others: Fail
*/
esp_err_t audio_stream_play(audio_stream_handle_t h, FILE *fp);
/**
* @brief Queue an audio file to be played on a stream
*
* Only supported for DECODER type streams.
*
* @param h Handle of the stream
* @param fp File pointer to the audio file
* @param play_now If true, start playing immediately (interrupting current playback)
* @return
* - ESP_OK: Success
* - ESP_ERR_NOT_SUPPORTED: Stream is not a decoder stream
* - Others: Fail
*/
esp_err_t audio_stream_queue(audio_stream_handle_t h, FILE *fp, bool play_now);
/**
* @brief Stop playback on a stream
*
* @param h Handle of the stream
* @return
* - ESP_OK: Success
* - Others: Fail
*/
esp_err_t audio_stream_stop(audio_stream_handle_t h);
/**
* @brief Pause playback on a stream
*
* Only supported for DECODER type streams.
*
* @param h Handle of the stream
* @return
* - ESP_OK: Success
* - ESP_ERR_NOT_SUPPORTED: Stream is not a decoder stream
* - Others: Fail
*/
esp_err_t audio_stream_pause(audio_stream_handle_t h);
/**
* @brief Resume playback on a stream
*
* Only supported for DECODER type streams.
*
* @param h Handle of the stream
* @return
* - ESP_OK: Success
* - ESP_ERR_NOT_SUPPORTED: Stream is not a decoder stream
* - Others: Fail
*/
esp_err_t audio_stream_resume(audio_stream_handle_t h);
/**
* @brief Direct write raw PCM data to a stream
*
* Only supported for RAW type streams.
* Data format must match the mixer configuration (e.g. 44.1kHz, 16-bit, mono/stereo).
*
* @param h Handle of the stream
* @param data Pointer to the PCM data
* @param size Size of the data in bytes
* @param timeout_ms Timeout in milliseconds to wait for space in the stream's buffer
* @return
* - ESP_OK: Success
* - ESP_ERR_NOT_SUPPORTED: Stream is not a raw stream
* - Others: Fail
*/
esp_err_t audio_stream_write_pcm(audio_stream_handle_t h, void *data, size_t size, uint32_t timeout_ms);
/**
* @brief Send an event to a raw stream's callback
*
* Allows manual state management for raw streams.
*
* @param h Handle of the stream
* @param event Event to send
* @return
* - ESP_OK: Success
* - ESP_ERR_NOT_SUPPORTED: Stream is not a raw stream
*/
esp_err_t audio_stream_raw_send_event(audio_stream_handle_t h, audio_player_callback_event_t event);
/**
* @brief Create a new audio stream
*
* @param cfg Pointer to the stream configuration structure
* @return Handle to the new stream, or NULL if failed
*/
audio_stream_handle_t audio_stream_new(audio_stream_config_t *cfg);
/**
* @brief Delete an audio stream and free its resources
*
* @param h Handle of the stream to delete
* @return
* - ESP_OK: Success
* - Others: Fail
*/
esp_err_t audio_stream_delete(audio_stream_handle_t h);
#ifdef __cplusplus
}
#endif

View file

@ -1,4 +1,4 @@
idf_component_register(SRC_DIRS "."
idf_component_register(SRCS "audio_player_test.c" "audio_mixer_test.c"
PRIV_INCLUDE_DIRS "."
PRIV_REQUIRES unity test_utils audio_player
EMBED_TXTFILES gs-16b-1c-44100hz.mp3)

View file

@ -0,0 +1,353 @@
#include <stdint.h>
#include "esp_log.h"
#include "esp_check.h"
#include "unity.h"
#include "audio_player.h"
#include "audio_mixer.h"
#include "audio_stream.h"
#include "driver/gpio.h"
#include "test_utils.h"
#include "freertos/semphr.h"
static const char *TAG = "AUDIO MIXER TEST";
#define CONFIG_BSP_I2S_NUM 1
/* Audio Pins (same as in audio_player_test.c) */
#define BSP_I2S_SCLK (GPIO_NUM_17)
#define BSP_I2S_MCLK (GPIO_NUM_2)
#define BSP_I2S_LCLK (GPIO_NUM_47)
#define BSP_I2S_DOUT (GPIO_NUM_15)
#define BSP_I2S_DSIN (GPIO_NUM_16)
#define BSP_POWER_AMP_IO (GPIO_NUM_46)
#define BSP_I2S_GPIO_CFG \
{ \
.mclk = BSP_I2S_MCLK, \
.bclk = BSP_I2S_SCLK, \
.ws = BSP_I2S_LCLK, \
.dout = BSP_I2S_DOUT, \
.din = BSP_I2S_DSIN, \
.invert_flags = { \
.mclk_inv = false, \
.bclk_inv = false, \
.ws_inv = false, \
}, \
}
static i2s_chan_handle_t i2s_tx_chan;
static i2s_chan_handle_t i2s_rx_chan;
static esp_err_t bsp_i2s_write(void * audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
return i2s_channel_write(i2s_tx_chan, (char *)audio_buffer, len, bytes_written, timeout_ms);
}
static esp_err_t bsp_i2s_reconfig_clk(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate),
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bits_cfg, (i2s_slot_mode_t)ch),
.gpio_cfg = BSP_I2S_GPIO_CFG,
};
i2s_channel_disable(i2s_tx_chan);
i2s_channel_reconfig_std_clock(i2s_tx_chan, &std_cfg.clk_cfg);
i2s_channel_reconfig_std_slot(i2s_tx_chan, &std_cfg.slot_cfg);
return i2s_channel_enable(i2s_tx_chan);
}
static esp_err_t bsp_audio_init(const i2s_std_config_t *i2s_config)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(CONFIG_BSP_I2S_NUM, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &i2s_tx_chan, &i2s_rx_chan));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(i2s_tx_chan, i2s_config));
ESP_ERROR_CHECK(i2s_channel_enable(i2s_tx_chan));
return ESP_OK;
}
static void bsp_audio_deinit()
{
i2s_channel_disable(i2s_tx_chan);
i2s_del_channel(i2s_tx_chan);
i2s_del_channel(i2s_rx_chan);
}
TEST_CASE("audio mixer can be initialized and deinitialized", "[audio mixer]")
{
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = BSP_I2S_GPIO_CFG,
};
TEST_ESP_OK(bsp_audio_init(&std_cfg));
audio_mixer_config_t mixer_cfg = {
.write_fn = bsp_i2s_write,
.clk_set_fn = bsp_i2s_reconfig_clk,
.priority = 5,
.coreID = 0,
.i2s_format = {
.sample_rate = 44100,
.bits_per_sample = 16,
.channels = 2
}
};
TEST_ESP_OK(audio_mixer_init(&mixer_cfg));
TEST_ASSERT_TRUE(audio_mixer_is_initialized());
audio_mixer_deinit();
TEST_ASSERT_FALSE(audio_mixer_is_initialized());
bsp_audio_deinit();
}
TEST_CASE("audio streams can be created and deleted", "[audio mixer]")
{
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = BSP_I2S_GPIO_CFG,
};
TEST_ESP_OK(bsp_audio_init(&std_cfg));
audio_mixer_config_t mixer_cfg = {
.write_fn = bsp_i2s_write,
.clk_set_fn = bsp_i2s_reconfig_clk,
.priority = 5,
.coreID = 0,
.i2s_format = {
.sample_rate = 44100,
.bits_per_sample = 16,
.channels = 2
}
};
TEST_ESP_OK(audio_mixer_init(&mixer_cfg));
// Create a decoder stream
audio_stream_config_t stream_cfg = DEFAULT_AUDIO_STREAM_CONFIG("decoder");
audio_stream_handle_t decoder_stream = audio_stream_new(&stream_cfg);
TEST_ASSERT_NOT_NULL(decoder_stream);
TEST_ASSERT_EQUAL(AUDIO_STREAM_TYPE_DECODER, audio_stream_get_type(decoder_stream));
TEST_ASSERT_EQUAL(1, audio_mixer_stream_count());
// Create a raw stream
audio_stream_config_t raw_cfg = {
.type = AUDIO_STREAM_TYPE_RAW,
.name = "raw",
.priority = 5,
.coreID = 0
};
audio_stream_handle_t raw_stream = audio_stream_new(&raw_cfg);
TEST_ASSERT_NOT_NULL(raw_stream);
TEST_ASSERT_EQUAL(AUDIO_STREAM_TYPE_RAW, audio_stream_get_type(raw_stream));
TEST_ASSERT_EQUAL(2, audio_mixer_stream_count());
// Delete streams
TEST_ESP_OK(audio_stream_delete(decoder_stream));
TEST_ASSERT_EQUAL(1, audio_mixer_stream_count());
TEST_ESP_OK(audio_stream_delete(raw_stream));
TEST_ASSERT_EQUAL(0, audio_mixer_stream_count());
audio_mixer_deinit();
bsp_audio_deinit();
}
TEST_CASE("audio mixer handles multiple streams and output format", "[audio mixer]")
{
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = BSP_I2S_GPIO_CFG,
};
TEST_ESP_OK(bsp_audio_init(&std_cfg));
audio_mixer_config_t mixer_cfg = {
.write_fn = bsp_i2s_write,
.clk_set_fn = bsp_i2s_reconfig_clk,
.priority = 5,
.coreID = 0,
.i2s_format = {
.sample_rate = 48000,
.bits_per_sample = 16,
.channels = 2
}
};
TEST_ESP_OK(audio_mixer_init(&mixer_cfg));
uint32_t rate, bits, ch;
audio_mixer_get_output_format(&rate, &bits, &ch);
TEST_ASSERT_EQUAL(48000, rate);
TEST_ASSERT_EQUAL(16, bits);
TEST_ASSERT_EQUAL(2, ch);
audio_stream_config_t s1_cfg = DEFAULT_AUDIO_STREAM_CONFIG("s1");
audio_stream_handle_t s1 = audio_stream_new(&s1_cfg);
(void)s1;
audio_stream_config_t s2_cfg = DEFAULT_AUDIO_STREAM_CONFIG("s2");
audio_stream_handle_t s2 = audio_stream_new(&s2_cfg);
(void)s2;
TEST_ASSERT_EQUAL(2, audio_mixer_stream_count());
audio_mixer_deinit(); // Should also clean up streams
TEST_ASSERT_EQUAL(0, audio_mixer_stream_count());
bsp_audio_deinit();
}
TEST_CASE("audio stream raw can send events", "[audio mixer]")
{
audio_stream_config_t raw_cfg = {
.type = AUDIO_STREAM_TYPE_RAW,
.name = "raw_event",
.priority = 5,
.coreID = 0
};
audio_stream_handle_t raw_stream = audio_stream_new(&raw_cfg);
TEST_ASSERT_NOT_NULL(raw_stream);
TEST_ASSERT_EQUAL(AUDIO_PLAYER_STATE_IDLE, audio_stream_get_state(raw_stream));
TEST_ESP_OK(audio_stream_raw_send_event(raw_stream, AUDIO_PLAYER_CALLBACK_EVENT_PLAYING));
TEST_ASSERT_EQUAL(AUDIO_PLAYER_STATE_PLAYING, audio_stream_get_state(raw_stream));
TEST_ESP_OK(audio_stream_raw_send_event(raw_stream, AUDIO_PLAYER_CALLBACK_EVENT_IDLE));
TEST_ASSERT_EQUAL(AUDIO_PLAYER_STATE_IDLE, audio_stream_get_state(raw_stream));
TEST_ESP_OK(audio_stream_delete(raw_stream));
}
static QueueHandle_t mixer_event_queue;
static void mixer_callback(audio_player_cb_ctx_t *ctx)
{
if (ctx->audio_event == AUDIO_PLAYER_CALLBACK_EVENT_PLAYING ||
ctx->audio_event == AUDIO_PLAYER_CALLBACK_EVENT_IDLE) {
xQueueSend(mixer_event_queue, &(ctx->audio_event), 0);
}
}
TEST_CASE("audio mixer plays sample mp3 on multiple streams", "[audio mixer]")
{
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = BSP_I2S_GPIO_CFG,
};
TEST_ESP_OK(bsp_audio_init(&std_cfg));
audio_mixer_config_t mixer_cfg = {
.write_fn = bsp_i2s_write,
.clk_set_fn = bsp_i2s_reconfig_clk,
.priority = 5,
.coreID = 0,
.i2s_format = {
.sample_rate = 44100,
.bits_per_sample = 16,
.channels = 2
}
};
TEST_ESP_OK(audio_mixer_init(&mixer_cfg));
mixer_event_queue = xQueueCreate(10, sizeof(audio_player_callback_event_t));
TEST_ASSERT_NOT_NULL(mixer_event_queue);
audio_mixer_callback_register(mixer_callback);
extern const char mp3_start[] asm("_binary_gs_16b_1c_44100hz_mp3_start");
extern const char mp3_end[] asm("_binary_gs_16b_1c_44100hz_mp3_end");
size_t mp3_size = (size_t)((uintptr_t)mp3_end - (uintptr_t)mp3_start);
// Create two streams
audio_stream_config_t s1_cfg = DEFAULT_AUDIO_STREAM_CONFIG("stream1");
audio_stream_handle_t s1 = audio_stream_new(&s1_cfg);
TEST_ASSERT_NOT_NULL(s1);
audio_stream_config_t s2_cfg = DEFAULT_AUDIO_STREAM_CONFIG("stream2");
audio_stream_handle_t s2 = audio_stream_new(&s2_cfg);
TEST_ASSERT_NOT_NULL(s2);
// Play on stream 1
FILE *f1 = fmemopen((void*)mp3_start, mp3_size, "rb");
TEST_ASSERT_NOT_NULL(f1);
TEST_ESP_OK(audio_stream_play(s1, f1));
// Play on stream 2
FILE *f2 = fmemopen((void*)mp3_start, mp3_size, "rb");
TEST_ASSERT_NOT_NULL(f2);
TEST_ESP_OK(audio_stream_play(s2, f2));
audio_player_callback_event_t event;
// We expect two PLAYING events (one for each stream)
int playing_count = 0;
while (playing_count < 2 && xQueueReceive(mixer_event_queue, &event, pdMS_TO_TICKS(500)) == pdPASS) {
if (event == AUDIO_PLAYER_CALLBACK_EVENT_PLAYING) {
playing_count++;
}
}
TEST_ASSERT_EQUAL(2, playing_count);
// Let it play for a few seconds
vTaskDelay(pdMS_TO_TICKS(2000));
// Stop streams
TEST_ESP_OK(audio_stream_stop(s1));
TEST_ESP_OK(audio_stream_stop(s2));
audio_mixer_deinit();
vQueueDelete(mixer_event_queue);
bsp_audio_deinit();
}
TEST_CASE("audio stream pause and resume", "[audio mixer]")
{
audio_stream_config_t stream_cfg = DEFAULT_AUDIO_STREAM_CONFIG("pause_resume");
audio_stream_handle_t s = audio_stream_new(&stream_cfg);
TEST_ASSERT_NOT_NULL(s);
TEST_ESP_OK(audio_stream_pause(s));
TEST_ASSERT_EQUAL(AUDIO_PLAYER_STATE_PAUSE, audio_stream_get_state(s));
TEST_ESP_OK(audio_stream_resume(s));
TEST_ASSERT_EQUAL(AUDIO_PLAYER_STATE_PLAYING, audio_stream_get_state(s));
TEST_ESP_OK(audio_stream_delete(s));
}
TEST_CASE("audio stream queue", "[audio mixer]")
{
audio_stream_config_t stream_cfg = DEFAULT_AUDIO_STREAM_CONFIG("queue");
audio_stream_handle_t s = audio_stream_new(&stream_cfg);
TEST_ASSERT_NOT_NULL(s);
extern const char mp3_start[] asm("_binary_gs_16b_1c_44100hz_mp3_start");
extern const char mp3_end[] asm("_binary_gs_16b_1c_44100hz_mp3_end");
size_t mp3_size = (size_t)((uintptr_t)mp3_end - (uintptr_t)mp3_start);
FILE *f1 = fmemopen((void*)mp3_start, mp3_size, "rb");
TEST_ASSERT_NOT_NULL(f1);
TEST_ESP_OK(audio_stream_queue(s, f1, false));
TEST_ESP_OK(audio_stream_delete(s));
}
TEST_CASE("audio stream write pcm", "[audio mixer]")
{
audio_stream_config_t raw_cfg = {
.type = AUDIO_STREAM_TYPE_RAW,
.name = "raw_write",
.priority = 5,
.coreID = 0
};
audio_stream_handle_t s = audio_stream_new(&raw_cfg);
TEST_ASSERT_NOT_NULL(s);
int16_t dummy_pcm[128] = {0};
TEST_ESP_OK(audio_stream_write_pcm(s, dummy_pcm, sizeof(dummy_pcm), 100));
TEST_ESP_OK(audio_stream_delete(s));
}