text/plain
•
13.04 KB
•
400 lines
#include "../../src/audio_player.h"
#include "../../src/audio_metadata.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 "driver/i2s_std.h"
#include "freertos/FreeRTOS.h"
#include <string.h>
static const char *TAG = "audio_player";
// I2S pins
#define PIN_BCK 13
#define PIN_WS 14
#define PIN_DOUT 21
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[64] = {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
// 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) {
// 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));
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 = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
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;
}
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 = 5,
.task_stack = 8192,
};
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, ".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[160];
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;
}
// Build URI: file://sdcard/path
char uri[160];
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';
}
void audio_player_pause(void) {
if (current_state == AUDIO_STATE_PLAYING) {
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;
}
current_state = AUDIO_STATE_PAUSED;
}
}
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;
}
if (using_ogg_decoder) {
ogg_opus_resume();
} else if (player) {
esp_audio_simple_player_resume(player);
}
current_state = AUDIO_STATE_PLAYING;
}
}
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_state == AUDIO_STATE_STOPPED || current_metadata.bitrate == 0) {
ESP_LOGW(TAG, "Cannot seek: ogg=%d, state=%d, bitrate=%lu",
using_ogg_decoder, current_state, (unsigned long)current_metadata.bitrate);
return false;
}
if (!player) {
return false;
}
// Calculate byte offset in the compressed file
uint64_t byte_offset = ((uint64_t)position_ms * current_metadata.bitrate) / 8000;
ESP_LOGI(TAG, "Seeking to %lums (byte offset %llu, bitrate=%lu)",
(unsigned long)position_ms, (unsigned long long)byte_offset,
(unsigned long)current_metadata.bitrate);
// Pipeline must be paused to seek
esp_gmf_err_t err = esp_audio_simple_player_pause(player);
if (err != ESP_GMF_ERR_OK) {
ESP_LOGE(TAG, "Failed to pause for seek: %d", err);
return false;
}
// Get the underlying pipeline and seek
esp_gmf_pipeline_handle_t pipe = NULL;
err = esp_audio_simple_player_get_pipeline(player, &pipe);
if (err != ESP_GMF_ERR_OK || !pipe) {
ESP_LOGE(TAG, "Failed to get pipeline: %d", err);
esp_audio_simple_player_resume(player);
return false;
}
err = esp_gmf_pipeline_seek(pipe, byte_offset);
if (err != ESP_GMF_ERR_OK) {
ESP_LOGE(TAG, "Pipeline seek failed: %d", err);
esp_audio_simple_player_resume(player);
return false;
}
// Update PCM position tracker to match the new position
// 48kHz stereo 16-bit = 192 bytes/ms
pcm_bytes_written = (uint64_t)position_ms * 192;
err = esp_audio_simple_player_resume(player);
if (err != ESP_GMF_ERR_OK) {
ESP_LOGE(TAG, "Failed to resume after seek: %d", err);
return false;
}
ESP_LOGI(TAG, "Seek complete");
return true;
}