Login
1 branch 0 tags
Ben (Desktop/Arch) Player screen cleanup 8ca4b4e 1 month ago 19 Commits
moon / firmware / esp32 / main / audio_player_esp32.c
#include "../../src/audio_player.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};

// 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);
        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",
                 info.sample_rate, info.channels, info.bits);

        // ESP-GMF resamples everything to 48000Hz (see ASP_POOL: Dest rate log)
        // So we just log the info but don't reconfigure I2S
        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;

    // Write a small silence buffer to flush DMA
    static const uint8_t silence[1024] = {0};
    size_t written = 0;
    i2s_channel_write(i2s_tx_chan, silence, sizeof(silence), &written, pdMS_TO_TICKS(50));
}

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

    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;
}

void audio_player_update(void) {
    // ESP-GMF handles everything in its own task
    // Nothing needed here
}