Files
lisael-box/main/audio/audio_player.c
T
Clément Saillant e35058f53d chore(box): commit firmware base + feature docs
Backfills the working firmware that had stayed uncommitted across the
build sessions, so the branch is self-contained.

- audio: audio_player, radio_pipeline, sd_player, calm_tone, aac_player
- net: podcast (RSS), udp_log
- modes: balloons, plus calm/games/radio tweaks
- ui: fonts header, icons; build files (CMakeLists, idf_component.yml,
  sdkconfig.defaults)
- docs/superpowers: Histoires design + plan

Note: sdkconfig stays gitignored (Wi-Fi creds + tuned LFN/FS/mbedtls
settings); a fresh checkout must reconfigure those.
2026-06-15 00:20:47 +02:00

193 lines
5.3 KiB
C

// Shared audio facade for Lisael Box — see audio_player.h.
#include "audio/audio_player.h"
#include "audio/radio_pipeline.h"
#include "audio/sd_player.h"
#include "audio/aac_player.h"
#include "lisael_config.h"
#include "esp_log.h"
#include "nvs.h"
#include "bsp/esp-box-3.h"
#include "esp_codec_dev.h"
#include "driver/i2s_std.h"
#include <audio_player.h> // chmorgan/esp-audio-player (angle brackets target
// its include dir, NOT our audio/audio_player.h)
static const char *TAG = "audio";
static esp_codec_dev_handle_t s_codec;
static lisael_audio_source_t s_source = LISAEL_AUDIO_IDLE;
static int s_volume = LISAEL_VOLUME_DEFAULT;
static bool s_player_ready;
static int clamp_volume(int v)
{
if (v < 0) v = 0;
if (v > LISAEL_VOLUME_MAX) v = LISAEL_VOLUME_MAX;
return v;
}
static void load_volume(void)
{
nvs_handle_t nvs;
if (nvs_open(LISAEL_NVS_NS, NVS_READONLY, &nvs) == ESP_OK) {
int32_t v;
if (nvs_get_i32(nvs, LISAEL_NVS_VOLUME, &v) == ESP_OK) {
s_volume = clamp_volume((int)v);
}
nvs_close(nvs);
}
}
static void persist_volume(void)
{
nvs_handle_t nvs;
if (nvs_open(LISAEL_NVS_NS, NVS_READWRITE, &nvs) == ESP_OK) {
nvs_set_i32(nvs, LISAEL_NVS_VOLUME, s_volume);
nvs_commit(nvs);
nvs_close(nvs);
}
}
// --- esp-audio-player <-> BSP codec glue ------------------------------------
// The player decodes MP3 to PCM and pushes it through these callbacks, which we
// back with the BSP's ES8311 codec device (esp_codec_dev). We do NOT touch I2S
// directly — the BSP owns it; we only (re)open the codec at the stream's rate.
static esp_err_t ap_mute(AUDIO_PLAYER_MUTE_SETTING setting)
{
if (s_codec) {
esp_codec_dev_set_out_mute(s_codec, setting == AUDIO_PLAYER_MUTE);
}
return ESP_OK;
}
static esp_err_t ap_clk_set(uint32_t rate, uint32_t bits, i2s_slot_mode_t ch)
{
if (!s_codec) {
return ESP_FAIL;
}
esp_codec_dev_close(s_codec); // harmless if not open
esp_codec_dev_sample_info_t fs = {
.bits_per_sample = (uint8_t)bits,
.channel = (ch == I2S_SLOT_MODE_STEREO) ? 2 : 1,
.sample_rate = rate,
};
esp_err_t err = esp_codec_dev_open(s_codec, &fs);
esp_codec_dev_set_out_vol(s_codec, (float)s_volume); // re-apply volume
return err;
}
static esp_err_t ap_write(void *buf, size_t len, size_t *written, uint32_t timeout_ms)
{
(void)timeout_ms;
int r = esp_codec_dev_write(s_codec, buf, (int)len);
*written = (r == ESP_CODEC_DEV_OK) ? len : 0;
return (r == ESP_CODEC_DEV_OK) ? ESP_OK : ESP_FAIL;
}
static void ap_event_cb(audio_player_cb_ctx_t *ctx)
{
if (ctx->audio_event == AUDIO_PLAYER_CALLBACK_EVENT_IDLE) {
// Playback finished (or was stopped): close the codec and mark idle.
if (s_codec) {
esp_codec_dev_close(s_codec);
}
if (s_source == LISAEL_AUDIO_SD) {
s_source = LISAEL_AUDIO_IDLE;
}
}
}
esp_err_t lisael_audio_player_ensure(void)
{
if (s_player_ready) {
return ESP_OK;
}
if (!s_codec) {
ESP_LOGE(TAG, "codec not ready — call lisael_audio_init first");
return ESP_FAIL;
}
audio_player_config_t cfg = {
.mute_fn = ap_mute,
.clk_set_fn = ap_clk_set,
.write_fn = ap_write,
.priority = 5,
.coreID = 1,
.force_stereo = false,
};
esp_err_t err = audio_player_new(cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "audio_player_new failed: %s", esp_err_to_name(err));
return err;
}
audio_player_callback_register(ap_event_cb, NULL);
s_player_ready = true;
ESP_LOGI(TAG, "esp-audio-player ready");
return ESP_OK;
}
esp_err_t lisael_audio_init(void)
{
// The BSP returns a ready-to-use ES8311 speaker codec device.
// bsp_i2c_init() must already have been called (done in app_main).
s_codec = bsp_audio_codec_speaker_init();
if (!s_codec) {
ESP_LOGE(TAG, "bsp_audio_codec_speaker_init failed");
return ESP_FAIL;
}
load_volume();
s_volume = LISAEL_VOLUME_MAX; // user request: start at maximum volume
persist_volume();
esp_codec_dev_set_out_vol(s_codec, (float)s_volume);
ESP_LOGI(TAG, "codec ready, volume=%d%% (cap %d%%)", s_volume, LISAEL_VOLUME_MAX);
return ESP_OK;
}
esp_codec_dev_handle_t lisael_audio_codec(void)
{
return s_codec;
}
void lisael_audio_stop_all(void)
{
switch (s_source) {
case LISAEL_AUDIO_RADIO: lisael_radio_stop(); break;
case LISAEL_AUDIO_SD: lisael_sd_stop(); break;
case LISAEL_AUDIO_PODCAST: lisael_aac_stop(); break;
default: break;
}
s_source = LISAEL_AUDIO_IDLE;
}
void lisael_audio_set_source(lisael_audio_source_t src)
{
s_source = src;
}
lisael_audio_source_t lisael_audio_get_source(void)
{
return s_source;
}
int lisael_audio_get_volume(void)
{
return s_volume;
}
void lisael_audio_set_volume(int percent)
{
s_volume = clamp_volume(percent);
if (s_codec) {
esp_codec_dev_set_out_vol(s_codec, (float)s_volume);
}
persist_volume();
ESP_LOGI(TAG, "volume -> %d%%", s_volume);
}
void lisael_audio_volume_step(int delta)
{
lisael_audio_set_volume(s_volume + delta);
}