wangzhibo
2026-09-11 659c470eba542383987aad227b7f918100f131fa
xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/mp3_player.cc
@@ -6,7 +6,9 @@
#include "application.h"
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <esp_audio_simple_dec.h>
@@ -18,32 +20,60 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#define TAG "sdcard_mp3"
#define MP3_READ_BUFFER_SIZE 512
#define MP3_OUTPUT_BUFFER_SIZE 4096
#define MP3_PLAYER_STACK_SIZE 8192
#define MP3_PLAYER_PRIORITY 5
#define MP3_PLAYER_PRIORITY 2
#define MP3_PSRAM_CACHE_MAX (768 * 1024)
typedef struct {
    char path[SDCARD_PATH_MAX_LEN];
    float speed;
    int repeat;
    bool power_off_after;
    int count;
    sdcard_mp3_playlist_item_t items[SDCARD_MP3_PLAYLIST_MAX];
} mp3_player_task_arg_t;
static TaskHandle_t mp3_player_task_handle;
static StackType_t *mp3_player_stack;
static StaticTask_t *mp3_player_tcb;
static volatile bool mp3_stop_requested;
static volatile bool mp3_pause_requested;
static volatile int mp3_repeat_left;
static TickType_t s_mp3_audible_started;
static SemaphoreHandle_t s_shadowing_listen_closed;
static std::vector<int16_t> convert_channels(const int16_t *input, size_t frame_count, uint8_t src_channels, uint8_t dst_channels)
static int16_t *s_pcm_a = nullptr;
static int16_t *s_pcm_b = nullptr;
static size_t s_pcm_a_cap = 0;
static size_t s_pcm_b_cap = 0;
static int16_t *ensure_psram_samples(int16_t **buf, size_t *cap, size_t samples)
{
    std::vector<int16_t> output(frame_count * dst_channels);
    if (samples == 0) {
        return *buf;
    }
    if (*buf && *cap >= samples) {
        return *buf;
    }
    int16_t *p = static_cast<int16_t *>(
        heap_caps_realloc(*buf, samples * sizeof(int16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
    if (!p) {
        return nullptr;
    }
    *buf = p;
    *cap = samples;
    return p;
}
static bool convert_channels(const int16_t *input, size_t frame_count, uint8_t src_channels, uint8_t dst_channels,
                             int16_t *output)
{
    if (!input || !output || frame_count == 0 || src_channels == 0 || dst_channels == 0) {
        return false;
    }
    if (src_channels == dst_channels) {
        std::memcpy(output.data(), input, output.size() * sizeof(int16_t));
        return output;
        std::memcpy(output, input, frame_count * dst_channels * sizeof(int16_t));
        return true;
    }
    for (size_t frame = 0; frame < frame_count; ++frame) {
@@ -63,24 +93,47 @@
            }
        }
    }
    return output;
    return true;
}
static bool is_supported_speed(float speed)
static size_t stretch_for_speed(const int16_t *input, size_t in_frames, uint8_t channels, float speed,
                                int16_t *output, size_t out_cap_frames)
{
    const float supported_speeds[] = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f};
    for (size_t i = 0; i < sizeof(supported_speeds) / sizeof(supported_speeds[0]); ++i) {
        float diff = speed - supported_speeds[i];
        if (diff < 0) {
            diff = -diff;
    if (!input || !output || channels == 0 || in_frames == 0) {
        return 0;
    }
    if (speed >= 0.999f && speed <= 1.001f) {
        size_t n = in_frames * channels;
        std::memcpy(output, input, n * sizeof(int16_t));
        return in_frames;
    }
    size_t out_frames = static_cast<size_t>(static_cast<double>(in_frames) / static_cast<double>(speed) + 0.5);
    if (out_frames < 1) {
        out_frames = 1;
    }
    if (out_frames > out_cap_frames) {
        out_frames = out_cap_frames;
    }
    for (size_t i = 0; i < out_frames; ++i) {
        const double src = static_cast<double>(i) * static_cast<double>(speed);
        size_t i0 = static_cast<size_t>(src);
        size_t i1 = i0 + 1;
        if (i0 >= in_frames) {
            i0 = in_frames - 1;
        }
        if (diff < 0.001f) {
            return true;
        if (i1 >= in_frames) {
            i1 = in_frames - 1;
        }
        const float t = static_cast<float>(src - static_cast<double>(i0));
        for (uint8_t c = 0; c < channels; ++c) {
            const float a = input[i0 * channels + c];
            const float b = input[i1 * channels + c];
            output[i * channels + c] = static_cast<int16_t>(a + (b - a) * t);
        }
    }
    return false;
    return out_frames;
}
static bool wait_if_paused()
{
@@ -90,8 +143,68 @@
    return !mp3_stop_requested;
}
static void wait_for_sd_dma(void)
{
    for (int i = 0; i < 20; ++i) {
        size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
        if (largest >= 4096) {
            return;
        }
        ESP_LOGW(TAG, "wait SD DMA largest=%u try=%d", (unsigned)largest, i);
        vTaskDelay(pdMS_TO_TICKS(40));
    }
}
static uint8_t *load_mp3_to_psram(const char *path, size_t *out_len)
{
    *out_len = 0;
    wait_for_sd_dma();
    FILE *fp = fopen(path, "rb");
    if (!fp) {
        return nullptr;
    }
    size_t cap = 32 * 1024;
    uint8_t *buf = static_cast<uint8_t *>(
        heap_caps_malloc(cap, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
    if (!buf) {
        fclose(fp);
        return nullptr;
    }
    size_t n = 0;
    while (n < MP3_PSRAM_CACHE_MAX) {
        if (n == cap) {
            size_t ncap = cap * 2;
            if (ncap > MP3_PSRAM_CACHE_MAX) {
                ncap = MP3_PSRAM_CACHE_MAX;
            }
            uint8_t *grown = static_cast<uint8_t *>(
                heap_caps_realloc(buf, ncap, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
            if (!grown) {
                break;
            }
            buf = grown;
            cap = ncap;
        }
        size_t got = fread(buf + n, 1, cap - n, fp);
        if (got == 0) {
            break;
        }
        n += got;
    }
    const bool at_eof = feof(fp) != 0;
    fclose(fp);
    if (n == 0 || !at_eof) {
        ESP_LOGW(TAG, "MP3 PSRAM cache miss n=%u eof=%d: %s", (unsigned)n, at_eof ? 1 : 0, path);
        heap_caps_free(buf);
        return nullptr;
    }
    *out_len = n;
    ESP_LOGI(TAG, "cached MP3 in PSRAM %u bytes: %s", (unsigned)n, path);
    return buf;
}
static bool output_pcm(AudioCodec *codec, esp_ae_rate_cvt_handle_t rate_cvt_handle, const uint8_t *buffer,
    uint32_t size, uint8_t src_channels, uint32_t src_rate)
    uint32_t size, uint8_t src_channels, uint32_t src_rate, float speed)
{
    uint8_t dst_channels = codec->output_channels();
    uint32_t dst_rate = codec->output_sample_rate();
@@ -100,65 +213,189 @@
        return true;
    }
    auto channel_converted = convert_channels(reinterpret_cast<const int16_t *>(buffer), frame_count, src_channels, dst_channels);
    int16_t *converted = ensure_psram_samples(&s_pcm_a, &s_pcm_a_cap, frame_count * dst_channels);
    if (!converted) {
        ESP_LOGE(TAG, "mp3 pcm alloc failed");
        return false;
    }
    if (!convert_channels(reinterpret_cast<const int16_t *>(buffer), frame_count, src_channels, dst_channels, converted)) {
        return false;
    }
    const int16_t *play = converted;
    size_t play_frames = frame_count;
    if (speed < 0.999f || speed > 1.001f) {
        size_t out_frames = static_cast<size_t>(static_cast<double>(frame_count) / static_cast<double>(speed) + 0.5);
        if (out_frames < 1) {
            out_frames = 1;
        }
        int16_t *stretched = ensure_psram_samples(&s_pcm_b, &s_pcm_b_cap, out_frames * dst_channels);
        if (!stretched) {
            ESP_LOGE(TAG, "mp3 stretch alloc failed");
            return false;
        }
        play_frames = stretch_for_speed(converted, frame_count, dst_channels, speed, stretched, out_frames);
        play = stretched;
    }
    if (play_frames == 0) {
        return true;
    }
    if (src_rate == dst_rate || !rate_cvt_handle) {
        codec->OutputData(channel_converted);
        codec->OutputPcm(play, static_cast<int>(play_frames * dst_channels));
        if (s_mp3_audible_started == 0) {
            s_mp3_audible_started = xTaskGetTickCount();
        }
        return true;
    }
    uint32_t max_out_frames = 0;
    if (esp_ae_rate_cvt_get_max_out_sample_num(rate_cvt_handle, frame_count, &max_out_frames) != ESP_AE_ERR_OK) {
    if (esp_ae_rate_cvt_get_max_out_sample_num(rate_cvt_handle, play_frames, &max_out_frames) != ESP_AE_ERR_OK) {
        ESP_LOGE(TAG, "get resample output size failed");
        return false;
    }
    std::vector<int16_t> resampled(max_out_frames * dst_channels);
    int16_t **out_slot = (play == s_pcm_b) ? &s_pcm_a : &s_pcm_b;
    size_t *out_cap = (play == s_pcm_b) ? &s_pcm_a_cap : &s_pcm_b_cap;
    int16_t *resampled = ensure_psram_samples(out_slot, out_cap, max_out_frames * dst_channels);
    if (!resampled) {
        ESP_LOGE(TAG, "mp3 resample alloc failed");
        return false;
    }
    uint32_t actual_out_frames = max_out_frames;
    if (esp_ae_rate_cvt_process(rate_cvt_handle, reinterpret_cast<esp_ae_sample_t>(channel_converted.data()), frame_count,
            reinterpret_cast<esp_ae_sample_t>(resampled.data()), &actual_out_frames) != ESP_AE_ERR_OK) {
    if (esp_ae_rate_cvt_process(rate_cvt_handle, reinterpret_cast<esp_ae_sample_t>(const_cast<int16_t *>(play)), play_frames,
            reinterpret_cast<esp_ae_sample_t>(resampled), &actual_out_frames) != ESP_AE_ERR_OK) {
        ESP_LOGE(TAG, "resample MP3 PCM failed");
        return false;
    }
    resampled.resize(actual_out_frames * dst_channels);
    codec->OutputData(resampled);
    codec->OutputPcm(resampled, static_cast<int>(actual_out_frames * dst_channels));
    if (s_mp3_audible_started == 0) {
        s_mp3_audible_started = xTaskGetTickCount();
    }
    return true;
}
static void pause_wake_for_sd(void)
{
    Application::GetInstance().Schedule([]() {
        auto &audio = Application::GetInstance().GetAudioService();
        audio.EnableVoiceProcessing(false);
        audio.EnableWakeWordDetection(false);
    });
    vTaskDelay(pdMS_TO_TICKS(40));
}
static void wait_shadowing_listen(unsigned duration_ms)
{
    unsigned wait_ms = Board_ShadowingListenWindowMs(duration_ms);
    // Server 听窗与设备同为 1.2×。多等 200ms,避免下一句课文开头被上一句 ASR 吃掉。
    wait_ms += 200;
    ESP_LOGI(TAG, "shadowing listen window %ums after clip %ums", wait_ms, duration_ms);
    unsigned waited = 0;
    while (waited < wait_ms && !mp3_stop_requested) {
        unsigned slice = wait_ms - waited;
        if (slice > 50) {
            slice = 50;
        }
        vTaskDelay(pdMS_TO_TICKS(slice));
        waited += slice;
    }
}
static void ensure_shadowing_listen_sem(void)
{
    if (s_shadowing_listen_closed == nullptr) {
        s_shadowing_listen_closed = xSemaphoreCreateBinary();
    }
}
static void wait_shadowing_mic_off(void)
{
    ensure_shadowing_listen_sem();
    xSemaphoreTake(s_shadowing_listen_closed, 0);
    Application::GetInstance().Schedule([]() {
        Application::GetInstance().CloseShadowingListenWindow();
        if (s_shadowing_listen_closed != nullptr) {
            xSemaphoreGive(s_shadowing_listen_closed);
        }
    });
    // 必须等 AFE 真正关掉再解码下一句。只 delay 80ms 会和录音重叠,内部 SRAM 会被打满。
    if (xSemaphoreTake(s_shadowing_listen_closed, pdMS_TO_TICKS(2500)) != pdTRUE) {
        ESP_LOGW(TAG, "shadowing wait mic off timeout, force AFE stop");
        Application::GetInstance().GetAudioService().EnableVoiceProcessing(false);
    }
}
static void mp3_player_task(void *arg)
{
    mp3_player_task_handle = xTaskGetCurrentTaskHandle();
    mp3_player_task_arg_t *task_arg = static_cast<mp3_player_task_arg_t *>(arg);
    char path[SDCARD_PATH_MAX_LEN];
    strlcpy(path, task_arg->path, sizeof(path));
    float speed = task_arg->speed;
    int repeat = task_arg->repeat < 1 ? 1 : task_arg->repeat;
    bool power_off_after = task_arg->power_off_after;
    free(task_arg);
    FILE *fp = fopen(path, "rb");
    if (!fp) {
        ESP_LOGE(TAG, "open MP3 failed: %s", path);
        mp3_player_task_handle = nullptr;
        mp3_stop_requested = false;
        mp3_repeat_left = 0;
        vTaskDelete(nullptr);
        return;
    sdcard_mp3_playlist_item_t items[SDCARD_MP3_PLAYLIST_MAX];
    int count = task_arg->count;
    if (count < 0) {
        count = 0;
    }
    if (count > SDCARD_MP3_PLAYLIST_MAX) {
        count = SDCARD_MP3_PLAYLIST_MAX;
    }
    if (count > 0) {
        memcpy(items, task_arg->items, sizeof(items[0]) * static_cast<size_t>(count));
    }
    bool power_off_after = count > 0 ? items[count - 1].power_off_after : false;
    heap_caps_free(task_arg);
    auto codec = Board::GetInstance().GetAudioCodec();
    if (!codec) {
        ESP_LOGE(TAG, "audio codec unavailable");
        fclose(fp);
    if (!codec || count < 1) {
        ESP_LOGE(TAG, "audio codec unavailable or empty playlist");
        mp3_player_task_handle = nullptr;
        mp3_stop_requested = false;
        mp3_repeat_left = 0;
        Board_OnSkillPlaybackFinished(true, false, 0);
        vTaskDelete(nullptr);
        return;
    }
    esp_audio_dec_register_default();
    esp_audio_simple_dec_register_default();
    uint8_t *in_buf = static_cast<uint8_t *>(
        heap_caps_malloc(MP3_READ_BUFFER_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
    uint8_t *out_buf = static_cast<uint8_t *>(
        heap_caps_malloc(MP3_OUTPUT_BUFFER_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
    uint32_t out_buf_size = MP3_OUTPUT_BUFFER_SIZE;
    bool output_enabled_before = codec->output_enabled();
    TickType_t play_started = xTaskGetTickCount();
    s_mp3_audible_started = 0;
    const bool shadowing = Board_IsShadowingTask();
    char path[SDCARD_PATH_MAX_LEN] = {0};
    if (!in_buf || !out_buf) {
        ESP_LOGE(TAG, "no memory for MP3 playback");
    }
    for (int item_i = 0; in_buf && out_buf && item_i < count && !mp3_stop_requested; ++item_i) {
    strlcpy(path, items[item_i].path, sizeof(path));
    float speed = SDCard_Clamp_MP3_Speed(items[item_i].speed);
    int repeat = items[item_i].repeat < 1 ? 1 : items[item_i].repeat;
    if (item_i > 0) {
        pause_wake_for_sd();
    }
    size_t file_len = 0;
    uint8_t *file_buf = load_mp3_to_psram(path, &file_len);
    FILE *fp = nullptr;
    if (!file_buf) {
        wait_for_sd_dma();
        fp = fopen(path, "rb");
    }
    if (!file_buf && !fp) {
        ESP_LOGE(TAG, "open MP3 failed: %s", path);
        continue;
    }
    if (!mp3_pause_requested) {
        Application::GetInstance().Schedule([]() {
            Application::GetInstance().ApplyMp3CommandOnlyMic();
        });
    }
    esp_audio_simple_dec_handle_t decoder = nullptr;
    esp_audio_simple_dec_cfg_t dec_cfg = {
@@ -171,30 +408,24 @@
    esp_audio_err_t ret = esp_audio_simple_dec_open(&dec_cfg, &decoder);
    if (ret != ESP_AUDIO_ERR_OK) {
        ESP_LOGE(TAG, "open MP3 decoder failed: %d", ret);
        esp_audio_simple_dec_unregister_default();
        esp_audio_dec_unregister_default();
        fclose(fp);
        mp3_player_task_handle = nullptr;
        mp3_stop_requested = false;
        mp3_repeat_left = 0;
        vTaskDelete(nullptr);
        return;
        if (fp) {
            fclose(fp);
            fp = nullptr;
        }
        if (file_buf) {
            heap_caps_free(file_buf);
            file_buf = nullptr;
        }
        continue;
    }
    uint8_t *in_buf = static_cast<uint8_t *>(malloc(MP3_READ_BUFFER_SIZE));
    uint8_t *out_buf = static_cast<uint8_t *>(malloc(MP3_OUTPUT_BUFFER_SIZE));
    uint32_t out_buf_size = MP3_OUTPUT_BUFFER_SIZE;
    bool output_enabled_before = codec->output_enabled();
    bool audio_info_checked = false;
    uint32_t mp3_sample_rate = 0;
    uint8_t mp3_channels = 0;
    esp_ae_rate_cvt_handle_t rate_cvt_handle = nullptr;
    if (!in_buf || !out_buf) {
        ESP_LOGE(TAG, "no memory for MP3 playback");
        ret = ESP_AUDIO_ERR_MEM_LACK;
    } else {
        ESP_LOGI(TAG, "play MP3: %s, speed: %.2fx, repeat: %d, power_off_after: %d", path, speed, repeat, power_off_after ? 1 : 0);
    ESP_LOGI(TAG, "play MP3: %s, speed: %.2fx, repeat: %d, item=%d/%d",
             path, speed, repeat, item_i + 1, count);
        if (wait_if_paused()) {
            codec->EnableOutput(true);
@@ -202,13 +433,26 @@
        for (int pass = 0; pass < repeat && ret == ESP_AUDIO_ERR_OK && !mp3_stop_requested; ++pass) {
            mp3_repeat_left = repeat - pass;
            s_mp3_audible_started = 0;
            size_t mem_off = 0;
            if (pass > 0) {
                if (fseek(fp, 0, SEEK_SET) != 0) {
                    ESP_LOGE(TAG, "rewind MP3 failed: %s", path);
                    break;
                if (!file_buf) {
                    if (fp) {
                        fclose(fp);
                        fp = nullptr;
                    }
                    vTaskDelay(pdMS_TO_TICKS(30));
                    wait_for_sd_dma();
                    fp = fopen(path, "rb");
                    if (!fp) {
                        ESP_LOGE(TAG, "reopen MP3 failed: %s", path);
                        break;
                    }
                }
                esp_audio_simple_dec_close(decoder);
                decoder = nullptr;
                if (decoder) {
                    esp_audio_simple_dec_close(decoder);
                    decoder = nullptr;
                }
                ret = esp_audio_simple_dec_open(&dec_cfg, &decoder);
                if (ret != ESP_AUDIO_ERR_OK || !decoder) {
                    ESP_LOGE(TAG, "reopen MP3 decoder failed: %d", ret);
@@ -221,7 +465,21 @@
                break;
            }
            int read_size = fread(in_buf, 1, MP3_READ_BUFFER_SIZE, fp);
            int read_size = 0;
            if (file_buf) {
                if (mem_off >= file_len) {
                    break;
                }
                size_t chunk = file_len - mem_off;
                if (chunk > MP3_READ_BUFFER_SIZE) {
                    chunk = MP3_READ_BUFFER_SIZE;
                }
                std::memcpy(in_buf, file_buf + mem_off, chunk);
                mem_off += chunk;
                read_size = static_cast<int>(chunk);
            } else {
                read_size = fread(in_buf, 1, MP3_READ_BUFFER_SIZE, fp);
            }
            if (read_size <= 0) {
                break;
            }
@@ -229,7 +487,7 @@
            esp_audio_simple_dec_raw_t raw = {
                .buffer = in_buf,
                .len = static_cast<uint32_t>(read_size),
                .eos = read_size < MP3_READ_BUFFER_SIZE,
                .eos = file_buf ? (mem_off >= file_len) : (read_size < MP3_READ_BUFFER_SIZE),
                .consumed = 0,
                .frame_recover = ESP_AUDIO_SIMPLE_DEC_RECOVERY_NONE,
            };
@@ -248,7 +506,8 @@
                ret = esp_audio_simple_dec_process(decoder, &raw, &out_frame);
                if (ret == ESP_AUDIO_ERR_BUFF_NOT_ENOUGH) {
                    uint8_t *new_buf = static_cast<uint8_t *>(realloc(out_buf, out_frame.needed_size));
                    uint8_t *new_buf = static_cast<uint8_t *>(
                        heap_caps_realloc(out_buf, out_frame.needed_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
                    if (!new_buf) {
                        ESP_LOGE(TAG, "resize MP3 output buffer failed");
                        ret = ESP_AUDIO_ERR_MEM_LACK;
@@ -274,7 +533,7 @@
                                ret = ESP_AUDIO_ERR_NOT_SUPPORT;
                                break;
                            }
                            mp3_sample_rate = static_cast<uint32_t>(info.sample_rate * speed);
                            mp3_sample_rate = info.sample_rate;
                            mp3_channels = info.channel;
                            if (mp3_sample_rate != static_cast<uint32_t>(codec->output_sample_rate())) {
                                esp_ae_rate_cvt_cfg_t rate_cvt_cfg = {
@@ -295,10 +554,11 @@
                        audio_info_checked = true;
                    }
                    if (!output_pcm(codec, rate_cvt_handle, out_frame.buffer, out_frame.decoded_size, mp3_channels, mp3_sample_rate)) {
                    if (!output_pcm(codec, rate_cvt_handle, out_frame.buffer, out_frame.decoded_size, mp3_channels, mp3_sample_rate, speed)) {
                        ret = ESP_AUDIO_ERR_FAIL;
                        break;
                    }
                    taskYIELD();
                }
                if (raw.consumed == 0) {
@@ -313,27 +573,55 @@
                break;
            }
        }
            if (shadowing && !mp3_stop_requested && ret == ESP_AUDIO_ERR_OK) {
                unsigned duration_ms = s_mp3_audible_started
                    ? pdTICKS_TO_MS(xTaskGetTickCount() - s_mp3_audible_started)
                    : 0;
                Board_OnShadowingClipFinished(duration_ms);
                wait_shadowing_listen(duration_ms);
                wait_shadowing_mic_off();
                s_mp3_audible_started = 0;
            }
        }
    }
    if (rate_cvt_handle) {
        esp_ae_rate_cvt_close(rate_cvt_handle);
        rate_cvt_handle = nullptr;
    }
    if (decoder) {
        esp_audio_simple_dec_close(decoder);
        decoder = nullptr;
    }
    if (fp) {
        fclose(fp);
        fp = nullptr;
    }
    if (file_buf) {
        heap_caps_free(file_buf);
        file_buf = nullptr;
    }
    ESP_LOGI(TAG, "MP3 item %s: %s", mp3_stop_requested ? "stopped" : "finished", path);
    }
    if (!output_enabled_before) {
        codec->EnableOutput(false);
    }
    free(in_buf);
    free(out_buf);
    if (decoder) {
        esp_audio_simple_dec_close(decoder);
    if (in_buf) {
        heap_caps_free(in_buf);
    }
    if (out_buf) {
        heap_caps_free(out_buf);
    }
    esp_audio_simple_dec_unregister_default();
    esp_audio_dec_unregister_default();
    fclose(fp);
    ESP_LOGI(TAG, "MP3 playback %s: %s", mp3_stop_requested ? "stopped" : "finished", path);
    ESP_LOGI(TAG, "MP3 playlist %s items=%d", mp3_stop_requested ? "stopped" : "finished", count);
    bool stopped = mp3_stop_requested;
    bool should_power_off = power_off_after && !stopped;
    unsigned duration_ms = s_mp3_audible_started
        ? pdTICKS_TO_MS(xTaskGetTickCount() - s_mp3_audible_started)
        : pdTICKS_TO_MS(xTaskGetTickCount() - play_started);
    mp3_player_task_handle = nullptr;
    mp3_stop_requested = false;
    mp3_pause_requested = false;
@@ -341,7 +629,11 @@
    if (should_power_off) {
        Application::GetInstance().RequestPowerOffStandby();
    }
    Board_OnSkillPlaybackFinished(stopped, power_off_after);
    if (shadowing && !stopped) {
        Board_OnShadowingPlaylistFinished();
    } else {
        Board_OnSkillPlaybackFinished(stopped, power_off_after, duration_ms);
    }
    vTaskDelete(nullptr);
}
@@ -357,11 +649,24 @@
extern "C" esp_err_t SDCard_Play_MP3_File_With_Speed_Repeat(const char *path, float speed, int repeat, bool power_off_after)
{
    if (!path || path[0] == '\0' || !is_supported_speed(speed)) {
    if (!path || path[0] == '\0') {
        return ESP_ERR_INVALID_ARG;
    }
    if (repeat < 1) {
        repeat = 1;
    sdcard_mp3_playlist_item_t item = {};
    strlcpy(item.path, path, sizeof(item.path));
    item.speed = speed;
    item.repeat = repeat;
    item.power_off_after = power_off_after;
    return SDCard_Play_MP3_File_Playlist(&item, 1);
}
extern "C" esp_err_t SDCard_Play_MP3_File_Playlist(const sdcard_mp3_playlist_item_t *items, int count)
{
    if (!items || count < 1) {
        return ESP_ERR_INVALID_ARG;
    }
    if (count > SDCARD_MP3_PLAYLIST_MAX) {
        count = SDCARD_MP3_PLAYLIST_MAX;
    }
    bool start_paused = mp3_pause_requested;
@@ -373,22 +678,46 @@
        }
    }
    mp3_player_task_arg_t *arg = static_cast<mp3_player_task_arg_t *>(calloc(1, sizeof(mp3_player_task_arg_t)));
    mp3_player_task_arg_t *arg = static_cast<mp3_player_task_arg_t *>(
        heap_caps_calloc(1, sizeof(mp3_player_task_arg_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
    if (!arg) {
        return ESP_ERR_NO_MEM;
    }
    strlcpy(arg->path, path, sizeof(arg->path));
    arg->speed = speed;
    arg->repeat = repeat;
    arg->power_off_after = power_off_after;
    arg->count = count;
    for (int i = 0; i < count; ++i) {
        strlcpy(arg->items[i].path, items[i].path, sizeof(arg->items[i].path));
        arg->items[i].speed = SDCard_Clamp_MP3_Speed(items[i].speed);
        arg->items[i].repeat = items[i].repeat < 1 ? 1 : items[i].repeat;
        arg->items[i].power_off_after = items[i].power_off_after;
    }
    mp3_stop_requested = false;
    mp3_pause_requested = start_paused;
    mp3_repeat_left = repeat;
    mp3_repeat_left = arg->items[0].repeat;
    BaseType_t ret = xTaskCreate(mp3_player_task, "mp3_player", MP3_PLAYER_STACK_SIZE, arg, MP3_PLAYER_PRIORITY, &mp3_player_task_handle);
    if (ret != pdPASS) {
        free(arg);
        mp3_player_task_handle = nullptr;
    if (mp3_player_stack == nullptr) {
        mp3_player_stack = static_cast<StackType_t *>(
            heap_caps_malloc(MP3_PLAYER_STACK_SIZE, MALLOC_CAP_SPIRAM));
    }
    if (mp3_player_tcb == nullptr) {
        mp3_player_tcb = static_cast<StaticTask_t *>(
            heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL));
    }
    if (mp3_player_stack == nullptr || mp3_player_tcb == nullptr) {
        ESP_LOGE(TAG, "mp3 task alloc failed internal=%u psram=%u",
                 (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
                 (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
        heap_caps_free(arg);
        mp3_repeat_left = 0;
        return ESP_ERR_NO_MEM;
    }
    mp3_player_task_handle = xTaskCreateStatic(
        mp3_player_task, "mp3_player", MP3_PLAYER_STACK_SIZE, arg,
        MP3_PLAYER_PRIORITY, mp3_player_stack, mp3_player_tcb);
    if (mp3_player_task_handle == nullptr) {
        ESP_LOGE(TAG, "mp3 xTaskCreateStatic failed internal=%u",
                 (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
        heap_caps_free(arg);
        mp3_repeat_left = 0;
        return ESP_FAIL;
    }
@@ -400,7 +729,6 @@
{
    mp3_pause_requested = false;
    if (!mp3_player_task_handle) {
        ESP_LOGW(TAG, "MP3 player is not running");
        return;
    }
@@ -415,6 +743,9 @@
extern "C" void SDCard_Resume_Play(void)
{
    mp3_pause_requested = false;
    Application::GetInstance().Schedule([]() {
        Application::GetInstance().ApplyMp3CommandOnlyMic();
    });
}
extern "C" bool SDCard_Is_Playing(void)