text/plain
•
16.01 KB
•
461 lines
#include "../../src/audio_player.h"
#include "../../src/audio_metadata.h"
#include "../../src/moon.h"
#include "pinout.h"
#include "ogg_opus_decoder.h"
#include "esp_log.h"
#include "esp_audio_simple_player.h"
#include "esp_audio_simple_player_advance.h"
#include "esp_gmf_pipeline.h"
#include "esp_gmf_io.h"
#include "driver/i2s_std.h"
#include "freertos/FreeRTOS.h"
#include <string.h>
static const char *TAG = "audio_player";
static esp_asp_handle_t player = NULL;
static i2s_chan_handle_t i2s_tx_chan = NULL;
static bool i2s_enabled = false;
static audio_state_t current_state = AUDIO_STATE_STOPPED;
static bool using_ogg_decoder = false; // true when using custom OGG Opus path
static char current_filename[STORAGE_MAX_NAME] = {0};
static char current_display_title[128] = {0}; // "Artist - Title" or filename
// Position/duration tracking
static audio_metadata_t current_metadata = {0}; // Metadata from file parsing
static uint64_t pcm_bytes_written = 0; // Accumulated PCM bytes written to I2S
// Seek state: stop/restart approach to avoid stale decoder buffers
static char current_path[STORAGE_MAX_PATH] = {0}; // Full filesystem path for restart
static uint64_t pending_seek_byte = 0; // Absolute file offset for pending seek
static uint32_t pending_seek_ms = 0; // Time position for PCM tracking
static bool seeking = false; // Suppress event callback during seek restart
// Audio output callback - writes decoded PCM to I2S
static int audio_output_cb(uint8_t *data, int size, void *ctx) {
if (i2s_tx_chan && data && size > 0) {
// Don't write or re-enable I2S when paused/stopped
if (current_state != AUDIO_STATE_PLAYING) return 0;
// Enable I2S on first write to avoid playing stale DMA buffer data
if (!i2s_enabled) {
// Preload silence to overwrite stale DMA buffer contents
uint8_t silence[256] = {0};
size_t loaded;
do {
loaded = 0;
i2s_channel_preload_data(i2s_tx_chan, silence, sizeof(silence), &loaded);
} while (loaded > 0);
i2s_channel_enable(i2s_tx_chan);
i2s_enabled = true;
}
pcm_bytes_written += (uint64_t)size;
size_t written = 0;
i2s_channel_write(i2s_tx_chan, data, size, &written, portMAX_DELAY);
return (int)written;
}
return 0;
}
// Event callback from simple player
static int player_event_cb(esp_asp_event_pkt_t *event, void *ctx) {
if (event->type == ESP_ASP_EVENT_TYPE_MUSIC_INFO) {
esp_asp_music_info_t info = {0};
memcpy(&info, event->payload, event->payload_size);
ESP_LOGI(TAG, "Music info: rate=%d, ch=%d, bits=%d, bitrate=%d",
info.sample_rate, info.channels, info.bits, (int)info.bitrate);
// ESP-GMF resamples everything to 48000Hz (see ASP_POOL: Dest rate log)
// Duration comes from metadata parsing, not from ESP-GMF
ESP_LOGI(TAG, "Source format: %dHz, %dch, %dbit (output resampled to 48000Hz)",
info.sample_rate, info.channels, info.bits);
} else if (event->type == ESP_ASP_EVENT_TYPE_STATE) {
esp_asp_state_t state = 0;
memcpy(&state, event->payload, event->payload_size);
ESP_LOGI(TAG, "Player state: %s", esp_audio_simple_player_state_to_str(state));
// Ignore state transitions during seek restart to prevent
// the UI from misinterpreting the stop/restart as playback end
if (seeking) {
ESP_LOGI(TAG, "Ignoring state change during seek: %s",
esp_audio_simple_player_state_to_str(state));
} else {
switch (state) {
case ESP_ASP_STATE_RUNNING:
current_state = AUDIO_STATE_PLAYING;
break;
case ESP_ASP_STATE_PAUSED:
current_state = AUDIO_STATE_PAUSED;
break;
case ESP_ASP_STATE_STOPPED:
case ESP_ASP_STATE_FINISHED:
current_state = AUDIO_STATE_STOPPED;
break;
case ESP_ASP_STATE_ERROR:
ESP_LOGE(TAG, "Playback error (file may be too large or unsupported)");
current_state = AUDIO_STATE_ERROR;
break;
default:
break;
}
}
}
return 0;
}
static bool init_i2s(void) {
if (i2s_tx_chan) {
return true;
}
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 8,
.dma_frame_num = 480,
};
esp_err_t err = i2s_new_channel(&chan_cfg, &i2s_tx_chan, NULL);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to create I2S channel: %s", esp_err_to_name(err));
return false;
}
// ESP-GMF resamples all audio to 48000Hz stereo 16-bit
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(48000),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = PIN_BCK,
.ws = PIN_WS,
.dout = PIN_DOUT,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
err = i2s_channel_init_std_mode(i2s_tx_chan, &std_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to init I2S std mode: %s", esp_err_to_name(err));
i2s_del_channel(i2s_tx_chan);
i2s_tx_chan = NULL;
return false;
}
ESP_LOGI(TAG, "I2S initialized (BCK=%d, WS=%d, DOUT=%d) at 48000Hz", PIN_BCK, PIN_WS, PIN_DOUT);
i2s_enabled = false; // Start disabled, enable on play
return true;
}
// Called before pipeline starts running - used to seek file IO for resume.
// We set the IO position before the file opens; _file_open checks info.pos
// and fseeks to it after fopen, so the decoder starts at the right offset.
static int player_prev_cb(esp_asp_handle_t *handle, void *ctx) {
(void)handle;
(void)ctx;
if (pending_seek_byte > 0 && player) {
esp_gmf_pipeline_handle_t pipe = NULL;
esp_gmf_err_t err = esp_audio_simple_player_get_pipeline(player, &pipe);
if (err == ESP_GMF_ERR_OK && pipe) {
esp_gmf_io_handle_t io = NULL;
err = esp_gmf_pipeline_get_in(pipe, &io);
if (err == ESP_GMF_ERR_OK && io) {
err = esp_gmf_io_set_pos(io, pending_seek_byte);
if (err != ESP_GMF_ERR_OK) {
ESP_LOGE(TAG, "Pre-run set_pos failed: %d", err);
} else {
ESP_LOGI(TAG, "Pre-run set_pos to byte %llu", (unsigned long long)pending_seek_byte);
}
} else {
ESP_LOGE(TAG, "Failed to get pipeline input IO: %d", err);
}
}
pcm_bytes_written = (uint64_t)pending_seek_ms * 192;
pending_seek_byte = 0;
pending_seek_ms = 0;
}
return 0;
}
static bool init_player(void) {
if (player) {
return true;
}
esp_asp_cfg_t cfg = {
.in.cb = NULL, // Use built-in file IO
.in.user_ctx = NULL,
.out.cb = audio_output_cb,
.out.user_ctx = NULL,
.task_prio = 15,
.task_stack = 8192,
.task_core = 1,
.prev = player_prev_cb,
};
esp_gmf_err_t err = esp_audio_simple_player_new(&cfg, &player);
if (err != ESP_GMF_ERR_OK || !player) {
ESP_LOGE(TAG, "Failed to create simple player: %d", err);
return false;
}
err = esp_audio_simple_player_set_event(player, player_event_cb, NULL);
if (err != ESP_GMF_ERR_OK) {
ESP_LOGE(TAG, "Failed to set event callback: %d", err);
esp_audio_simple_player_destroy(player);
player = NULL;
return false;
}
ESP_LOGI(TAG, "ESP-GMF audio simple player initialized");
return true;
}
static bool is_ogg_file(const char *path) {
const char *ext = strrchr(path, '.');
if (!ext) return false;
return (strcasecmp(ext, ".opus") == 0 || strcasecmp(ext, ".opu") == 0 || strcasecmp(ext, ".ogg") == 0);
}
bool audio_player_init(void) {
ESP_LOGI(TAG, "Audio player init");
return true; // Lazy initialization
}
bool audio_player_play(const char *path) {
audio_player_stop();
// Reset position tracking
pcm_bytes_written = 0;
memset(¤t_metadata, 0, sizeof(current_metadata));
// Get audio metadata (duration, tags, etc.)
char full_path[STORAGE_MAX_PATH + 8];
snprintf(full_path, sizeof(full_path), "/sdcard%s", path);
if (audio_metadata_get(full_path, ¤t_metadata)) {
ESP_LOGI(TAG, "Metadata: duration=%lums, bitrate=%lu, title=%s, artist=%s",
(unsigned long)current_metadata.duration_ms,
(unsigned long)current_metadata.bitrate,
current_metadata.title,
current_metadata.artist);
// Build display title: "Artist - Title" or just "Title" or filename
if (current_metadata.artist[0] && current_metadata.title[0]) {
snprintf(current_display_title, sizeof(current_display_title), "%.60s - %.60s",
current_metadata.artist, current_metadata.title);
} else if (current_metadata.title[0]) {
snprintf(current_display_title, sizeof(current_display_title), "%.127s",
current_metadata.title);
} else {
current_display_title[0] = '\0'; // Will fall back to filename
}
} else {
ESP_LOGW(TAG, "Failed to parse metadata for: %s", full_path);
current_display_title[0] = '\0';
}
if (!init_i2s()) {
return false;
}
// Extract filename for display
const char *name = strrchr(path, '/');
name = name ? name + 1 : path;
strncpy(current_filename, name, sizeof(current_filename) - 1);
current_filename[sizeof(current_filename) - 1] = '\0';
// OGG/Opus files need custom decoder (ESP-GMF has no OGG demuxer)
if (is_ogg_file(path)) {
ESP_LOGI(TAG, "Playing (OGG Opus): %s", full_path);
using_ogg_decoder = true;
if (!ogg_opus_start(full_path, audio_output_cb, NULL)) {
ESP_LOGE(TAG, "Failed to start OGG Opus decoder");
using_ogg_decoder = false;
return false;
}
current_state = AUDIO_STATE_PLAYING;
return true;
}
// I2S is enabled lazily in audio_output_cb to avoid playing stale DMA data
if (!init_player()) {
return false;
}
// Save path for seek restart
snprintf(current_path, sizeof(current_path), "%s", path);
// Build URI: file://sdcard/path
char uri[STORAGE_MAX_PATH + 16];
snprintf(uri, sizeof(uri), "file://sdcard%s", path);
ESP_LOGI(TAG, "Playing: %s", uri);
esp_gmf_err_t err = esp_audio_simple_player_run(player, uri, NULL);
if (err != ESP_GMF_ERR_OK) {
ESP_LOGE(TAG, "Failed to start playback: %d", err);
return false;
}
current_state = AUDIO_STATE_PLAYING;
return true;
}
void audio_player_stop(void) {
if (using_ogg_decoder) {
ogg_opus_stop();
using_ogg_decoder = false;
} else if (player && current_state != AUDIO_STATE_STOPPED) {
esp_audio_simple_player_stop(player);
}
// Disable I2S to stop any residual noise
if (i2s_tx_chan && i2s_enabled) {
i2s_channel_disable(i2s_tx_chan);
i2s_enabled = false;
}
current_state = AUDIO_STATE_STOPPED;
current_filename[0] = '\0';
current_path[0] = '\0';
}
void audio_player_pause(void) {
if (current_state == AUDIO_STATE_PLAYING) {
// Set state first so audio_output_cb stops writing and won't re-enable I2S
current_state = AUDIO_STATE_PAUSED;
if (using_ogg_decoder) {
ogg_opus_pause();
} else if (player) {
esp_audio_simple_player_pause(player);
}
// Disable I2S to stop residual noise
if (i2s_tx_chan && i2s_enabled) {
i2s_channel_disable(i2s_tx_chan);
i2s_enabled = false;
}
}
}
void audio_player_resume(void) {
if (current_state == AUDIO_STATE_PAUSED) {
// Re-enable I2S before resuming
if (i2s_tx_chan && !i2s_enabled) {
i2s_channel_enable(i2s_tx_chan);
i2s_enabled = true;
}
// Set state before resuming task so audio_output_cb accepts writes immediately
current_state = AUDIO_STATE_PLAYING;
if (using_ogg_decoder) {
ogg_opus_resume();
} else if (player) {
esp_audio_simple_player_resume(player);
}
}
}
audio_state_t audio_player_get_state(void) {
return current_state;
}
void audio_player_set_volume(uint8_t volume) {
// Volume control would require ALC element in the pipeline
// For now, this is a placeholder
(void)volume;
}
const char *audio_player_get_filename(void) {
return current_filename[0] ? current_filename : NULL;
}
const char *audio_player_get_title(void) {
if (current_display_title[0]) {
return current_display_title;
}
return current_filename[0] ? current_filename : NULL;
}
void audio_player_update(void) {
// ESP-GMF handles everything in its own task
// Nothing needed here
}
uint32_t audio_player_get_position_ms(void) {
// ESP-GMF resamples to 48kHz stereo 16-bit
// 48000 Hz * 2 channels * 2 bytes = 192000 bytes/sec = 192 bytes/ms
return (uint32_t)(pcm_bytes_written / 192);
}
uint32_t audio_player_get_duration_ms(void) {
return current_metadata.duration_ms;
}
uint8_t audio_player_get_progress(void) {
if (current_metadata.duration_ms == 0) {
return 0;
}
uint32_t position = audio_player_get_position_ms();
uint32_t progress = (position * 100) / current_metadata.duration_ms;
return progress > 100 ? 100 : (uint8_t)progress;
}
bool audio_player_seek_ms(uint32_t position_ms) {
if (using_ogg_decoder || current_metadata.bitrate == 0 || current_path[0] == '\0') {
ESP_LOGW(TAG, "Cannot seek: ogg=%d, bitrate=%lu, path=%s",
using_ogg_decoder, (unsigned long)current_metadata.bitrate,
current_path[0] ? current_path : "(none)");
return false;
}
if (!player) {
return false;
}
// Calculate absolute byte offset: audio_data_offset + compressed audio offset
uint64_t audio_offset = ((uint64_t)position_ms * current_metadata.bitrate) / 8000;
uint64_t absolute_offset = current_metadata.audio_data_offset + audio_offset;
ESP_LOGI(TAG, "Seeking to %lums (data_offset=%lu + audio=%llu = abs=%llu, bitrate=%lu)",
(unsigned long)position_ms,
(unsigned long)current_metadata.audio_data_offset,
(unsigned long long)audio_offset,
(unsigned long long)absolute_offset,
(unsigned long)current_metadata.bitrate);
// Store pending seek for the prev callback
pending_seek_byte = absolute_offset;
pending_seek_ms = position_ms;
// Set seeking flag to suppress event callback state changes during restart
seeking = true;
// Disable I2S to avoid noise during restart
if (i2s_tx_chan && i2s_enabled) {
i2s_channel_disable(i2s_tx_chan);
i2s_enabled = false;
}
// Stop the pipeline (but don't clear metadata/filename/path)
esp_audio_simple_player_stop(player);
// Rebuild URI and restart - the prev callback will seek before decode starts
char uri[STORAGE_MAX_PATH + 16];
snprintf(uri, sizeof(uri), "file://sdcard%s", current_path);
ESP_LOGI(TAG, "Restarting pipeline for seek: %s", uri);
esp_gmf_err_t err = esp_audio_simple_player_run(player, uri, NULL);
if (err != ESP_GMF_ERR_OK) {
ESP_LOGE(TAG, "Failed to restart for seek: %d", err);
seeking = false;
pending_seek_byte = 0;
pending_seek_ms = 0;
current_state = AUDIO_STATE_ERROR;
return false;
}
seeking = false;
current_state = AUDIO_STATE_PLAYING;
ESP_LOGI(TAG, "Seek complete (stop/restart)");
return true;
}