Login
1 branch 0 tags
Ben (Desktop/Arch) Code cleanup b2535ca 1 month ago 24 Commits
moon / firmware / esp32 / main / audio_player_esp32.c
#include "../../src/audio_player.h"
#include "../../src/audio_metadata.h"
#include "esp_log.h"
#include "esp_audio_simple_player.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 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) {
        size_t written = 0;
        // Use portMAX_DELAY to ensure proper backpressure from I2S DMA
        i2s_channel_write(i2s_tx_chan, data, size, &written, portMAX_DELAY);
        pcm_bytes_written += written;
        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;
}

bool audio_player_init(void) {
    ESP_LOGI(TAG, "Audio player init");
    return true;  // Lazy initialization
}

// Write silence to I2S to clear any residual audio
static void flush_i2s_buffer(void) {
    if (!i2s_tx_chan) return;
}

bool audio_player_play(const char *path) {
    audio_player_stop();

    // Reset position tracking
    pcm_bytes_written = 0;
    memset(&current_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, &current_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;
    }

    // Enable I2S if not already enabled, and flush buffer
    if (!i2s_enabled) {
        i2s_channel_enable(i2s_tx_chan);
        i2s_enabled = true;
    }
    flush_i2s_buffer();

    if (!init_player()) {
        return false;
    }

    // Build URI: file://sdcard/path
    char uri[160];
    snprintf(uri, sizeof(uri), "file://sdcard%s", path);

    // 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';

    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 (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 (player && current_state == AUDIO_STATE_PLAYING) {
        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 (player && 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;
        }
        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;
}