#include "system.h" #include "app_task.h" #include "audio_codec.h" #include "board.h" #include "application.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #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 2 #define MP3_PSRAM_CACHE_MAX (768 * 1024) typedef struct { int count; sdcard_mp3_playlist_item_t items[]; } 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 volatile bool s_mp3_offline_wake_ok; 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) { if (samples == 0) { return *buf; } if (*buf && *cap >= samples) { return *buf; } int16_t *p = static_cast( 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, input, frame_count * dst_channels * sizeof(int16_t)); return true; } for (size_t frame = 0; frame < frame_count; ++frame) { if (dst_channels == 1) { int32_t mixed = 0; for (uint8_t ch = 0; ch < src_channels; ++ch) { mixed += input[frame * src_channels + ch]; } output[frame] = static_cast(mixed / src_channels); } else if (src_channels == 1) { for (uint8_t ch = 0; ch < dst_channels; ++ch) { output[frame * dst_channels + ch] = input[frame]; } } else { for (uint8_t ch = 0; ch < dst_channels; ++ch) { output[frame * dst_channels + ch] = input[frame * src_channels + std::min(ch, src_channels - 1)]; } } } return true; } 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) { 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(static_cast(in_frames) / static_cast(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(i) * static_cast(speed); size_t i0 = static_cast(src); size_t i1 = i0 + 1; if (i0 >= in_frames) { i0 = in_frames - 1; } if (i1 >= in_frames) { i1 = in_frames - 1; } const float t = static_cast(src - static_cast(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(a + (b - a) * t); } } return out_frames; } static bool wait_if_paused() { while (mp3_pause_requested && !mp3_stop_requested) { vTaskDelay(pdMS_TO_TICKS(20)); } return !mp3_stop_requested; } static bool wait_for_internal_sram(size_t need_bytes, int tries) { for (int i = 0; i < tries; ++i) { size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); if (largest >= need_bytes) { return true; } ESP_LOGW(TAG, "wait SD DMA largest=%u need=%u try=%d", (unsigned)largest, (unsigned)need_bytes, i); vTaskDelay(pdMS_TO_TICKS(40)); } return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA) >= need_bytes; } static bool sd_internal_heap_ok_for_fopen(void) { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA) >= 4096 && heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) >= 2048; } static FILE *fopen_mp3_if_heap_ok(const char *path) { wait_for_internal_sram(4096, 25); if (!sd_internal_heap_ok_for_fopen()) { ESP_LOGE(TAG, "refuse fopen, internal DMA heap too small: %s", path); return nullptr; } return fopen(path, "rb"); } static void ensure_shadowing_listen_sem(void); static void close_mp3_decode_resources(esp_audio_simple_dec_handle_t *decoder, esp_ae_rate_cvt_handle_t *rate_cvt, FILE **fp, uint8_t **file_buf) { if (rate_cvt && *rate_cvt) { esp_ae_rate_cvt_close(*rate_cvt); *rate_cvt = nullptr; } if (decoder && *decoder) { esp_audio_simple_dec_close(*decoder); *decoder = nullptr; } if (fp && *fp) { fclose(*fp); *fp = nullptr; } if (file_buf && *file_buf) { heap_caps_free(*file_buf); *file_buf = nullptr; } } static void apply_stop_afe_for_sd(bool leave_listening) { auto &app = Application::GetInstance(); if (leave_listening) { app.HoldIdleForServerPrompt(); if (app.GetDeviceState() == kDeviceStateSpeaking) { app.AbortSpeaking(kAbortReasonNone); } if (app.GetDeviceState() == kDeviceStateListening) { app.SetDeviceState(kDeviceStateIdle); } } auto &audio = app.GetAudioService(); audio.EnableVoiceProcessing(false); audio.EnableWakeWordDetection(false); audio.ReleaseAfeRuntime(); } static void stop_afe_for_sd_and_wait(bool leave_listening) { s_mp3_offline_wake_ok = false; auto &app = Application::GetInstance(); if (app.IsOnMainLoop()) { apply_stop_afe_for_sd(leave_listening); wait_for_internal_sram(4096, 40); return; } ensure_shadowing_listen_sem(); xSemaphoreTake(s_shadowing_listen_closed, 0); app.Schedule([leave_listening]() { apply_stop_afe_for_sd(leave_listening); if (s_shadowing_listen_closed != nullptr) { xSemaphoreGive(s_shadowing_listen_closed); } }); if (xSemaphoreTake(s_shadowing_listen_closed, pdMS_TO_TICKS(2500)) != pdTRUE) { ESP_LOGW(TAG, "pause wake for SD timeout"); } wait_for_internal_sram(4096, 40); } static void pause_wake_for_sd(void) { stop_afe_for_sd_and_wait(false); } static uint8_t *load_mp3_to_psram(const char *path, size_t *out_len) { *out_len = 0; FILE *fp = fopen_mp3_if_heap_ok(path); if (!fp) { return nullptr; } size_t cap = 32 * 1024; uint8_t *buf = static_cast( 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( 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, float speed) { uint8_t dst_channels = codec->output_channels(); uint32_t dst_rate = codec->output_sample_rate(); size_t frame_count = size / (sizeof(int16_t) * src_channels); if (frame_count == 0) { return true; } 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(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(static_cast(frame_count) / static_cast(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->OutputPcm(play, static_cast(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, play_frames, &max_out_frames) != ESP_AE_ERR_OK) { ESP_LOGE(TAG, "get resample output size failed"); return false; } 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(const_cast(play)), play_frames, reinterpret_cast(resampled), &actual_out_frames) != ESP_AE_ERR_OK) { ESP_LOGE(TAG, "resample MP3 PCM failed"); return false; } codec->OutputPcm(resampled, static_cast(actual_out_frames * dst_channels)); if (s_mp3_audible_started == 0) { s_mp3_audible_started = xTaskGetTickCount(); } return true; } 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); Application::GetInstance().GetAudioService().EnableWakeWordDetection(false); } } static void mp3_player_task(void *arg) { mp3_player_task_handle = xTaskGetCurrentTaskHandle(); mp3_player_task_arg_t *task_arg = static_cast(arg); int count = task_arg ? task_arg->count : 0; if (count < 0) { count = 0; } if (count > SDCARD_MP3_PLAYLIST_MAX) { count = SDCARD_MP3_PLAYLIST_MAX; } const sdcard_mp3_playlist_item_t *items = (task_arg && count > 0) ? task_arg->items : nullptr; bool power_off_after = (items != nullptr) ? items[count - 1].power_off_after : false; auto codec = Board::GetInstance().GetAudioCodec(); if (!codec || count < 1 || items == nullptr) { ESP_LOGE(TAG, "audio codec unavailable or empty playlist"); if (task_arg) { heap_caps_free(task_arg); } mp3_player_task_handle = nullptr; mp3_stop_requested = false; mp3_repeat_left = 0; s_mp3_offline_wake_ok = false; 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( heap_caps_malloc(MP3_READ_BUFFER_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); uint8_t *out_buf = static_cast( 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"); } s_mp3_offline_wake_ok = false; 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; pause_wake_for_sd(); size_t file_len = 0; uint8_t *file_buf = load_mp3_to_psram(path, &file_len); if (!file_buf) { pause_wake_for_sd(); file_buf = load_mp3_to_psram(path, &file_len); } FILE *fp = nullptr; if (!file_buf) { fp = fopen_mp3_if_heap_ok(path); } if (!file_buf && !fp) { ESP_LOGE(TAG, "open MP3 failed: %s", path); continue; } // 先把文件读进 PSRAM,再开离线口令唤醒。反过来会把 SD DMA 需要的连续内部 SRAM 吃掉。 if (file_buf) { s_mp3_offline_wake_ok = true; if (!mp3_pause_requested) { Application::GetInstance().Schedule([]() { Application::GetInstance().ApplyMp3CommandOnlyMic(); }); } } else { s_mp3_offline_wake_ok = false; } esp_audio_simple_dec_handle_t decoder = nullptr; esp_audio_simple_dec_cfg_t dec_cfg = { .dec_type = ESP_AUDIO_SIMPLE_DEC_TYPE_MP3, .dec_cfg = nullptr, .cfg_size = 0, .use_frame_dec = false, }; 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); if (fp) { fclose(fp); fp = nullptr; } if (file_buf) { heap_caps_free(file_buf); file_buf = nullptr; } continue; } 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; 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); } 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 (!file_buf) { if (fp) { fclose(fp); fp = nullptr; } vTaskDelay(pdMS_TO_TICKS(30)); fp = fopen_mp3_if_heap_ok(path); if (!fp) { ESP_LOGE(TAG, "reopen MP3 failed: %s", path); break; } } 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); break; } } while (ret == ESP_AUDIO_ERR_OK && !mp3_stop_requested) { if (!wait_if_paused()) { break; } 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(chunk); } else { read_size = fread(in_buf, 1, MP3_READ_BUFFER_SIZE, fp); } if (read_size <= 0) { break; } esp_audio_simple_dec_raw_t raw = { .buffer = in_buf, .len = static_cast(read_size), .eos = file_buf ? (mem_off >= file_len) : (read_size < MP3_READ_BUFFER_SIZE), .consumed = 0, .frame_recover = ESP_AUDIO_SIMPLE_DEC_RECOVERY_NONE, }; while (raw.len > 0 && !mp3_stop_requested) { if (!wait_if_paused()) { break; } esp_audio_simple_dec_out_t out_frame = { .buffer = out_buf, .len = out_buf_size, .needed_size = 0, .decoded_size = 0, }; ret = esp_audio_simple_dec_process(decoder, &raw, &out_frame); if (ret == ESP_AUDIO_ERR_BUFF_NOT_ENOUGH) { uint8_t *new_buf = static_cast( 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; break; } out_buf = new_buf; out_buf_size = out_frame.needed_size; continue; } if (ret != ESP_AUDIO_ERR_OK) { ESP_LOGE(TAG, "decode MP3 failed: %d", ret); break; } if (out_frame.decoded_size > 0) { if (!audio_info_checked) { esp_audio_simple_dec_info_t info = {}; if (esp_audio_simple_dec_get_info(decoder, &info) == ESP_AUDIO_ERR_OK) { ESP_LOGI(TAG, "MP3 info: %lu Hz, %u bits, %u ch", info.sample_rate, info.bits_per_sample, info.channel); if (info.bits_per_sample != 16 || info.channel == 0 || codec->output_channels() == 0) { ESP_LOGE(TAG, "MP3 format unsupported: %lu Hz, %u bits, %u ch", info.sample_rate, info.bits_per_sample, info.channel); ret = ESP_AUDIO_ERR_NOT_SUPPORT; break; } mp3_sample_rate = info.sample_rate; mp3_channels = info.channel; if (mp3_sample_rate != static_cast(codec->output_sample_rate())) { esp_ae_rate_cvt_cfg_t rate_cvt_cfg = { .src_rate = mp3_sample_rate, .dest_rate = static_cast(codec->output_sample_rate()), .channel = static_cast(codec->output_channels()), .bits_per_sample = 16, .complexity = 2, .perf_type = ESP_AE_RATE_CVT_PERF_TYPE_SPEED, }; if (esp_ae_rate_cvt_open(&rate_cvt_cfg, &rate_cvt_handle) != ESP_AE_ERR_OK || !rate_cvt_handle) { ESP_LOGE(TAG, "open MP3 resampler failed"); ret = ESP_AUDIO_ERR_NOT_SUPPORT; break; } } } audio_info_checked = true; } 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) { break; } raw.len -= raw.consumed; raw.buffer += raw.consumed; raw.consumed = 0; } if (raw.eos || ret != ESP_AUDIO_ERR_OK) { 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; // 听窗要开 AFE。先关掉 MP3 解码器,避免和录音抢内部 SRAM。 close_mp3_decode_resources(&decoder, &rate_cvt_handle, &fp, &file_buf); audio_info_checked = false; mp3_sample_rate = 0; mp3_channels = 0; Board_OnShadowingClipFinished(duration_ms); wait_shadowing_listen(duration_ms); wait_shadowing_mic_off(); s_mp3_audible_started = 0; } } close_mp3_decode_resources(&decoder, &rate_cvt_handle, &fp, &file_buf); ESP_LOGI(TAG, "MP3 item %s: %s", mp3_stop_requested ? "stopped" : "finished", path); } if (!output_enabled_before) { codec->EnableOutput(false); } 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(); 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); if (task_arg) { heap_caps_free(task_arg); task_arg = nullptr; } mp3_player_task_handle = nullptr; mp3_stop_requested = false; mp3_pause_requested = false; mp3_repeat_left = 0; s_mp3_offline_wake_ok = false; if (should_power_off) { Application::GetInstance().RequestPowerOffStandby(); } if (shadowing && !stopped) { Board_OnShadowingPlaylistFinished(); } else { Board_OnSkillPlaybackFinished(stopped, power_off_after, duration_ms); } vTaskDelete(nullptr); } extern "C" esp_err_t SDCard_Play_MP3_File(const char *path) { return SDCard_Play_MP3_File_With_Speed_Repeat(path, 1.0f, 1, false); } extern "C" esp_err_t SDCard_Play_MP3_File_With_Speed(const char *path, float speed) { return SDCard_Play_MP3_File_With_Speed_Repeat(path, speed, 1, false); } 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') { return ESP_ERR_INVALID_ARG; } 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) { ESP_LOGW(TAG, "playlist truncated incoming=%d max=%d", count, SDCARD_MP3_PLAYLIST_MAX); count = SDCARD_MP3_PLAYLIST_MAX; } bool start_paused = mp3_pause_requested; if (mp3_player_task_handle) { mp3_stop_requested = true; mp3_pause_requested = false; while (mp3_player_task_handle) { vTaskDelay(pdMS_TO_TICKS(20)); } } mp3_player_task_arg_t *arg = static_cast( heap_caps_calloc(1, sizeof(mp3_player_task_arg_t) + sizeof(sdcard_mp3_playlist_item_t) * static_cast(count), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); if (!arg) { return ESP_ERR_NO_MEM; } 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 = arg->items[0].repeat; if (mp3_player_stack == nullptr) { mp3_player_stack = static_cast( heap_caps_malloc(MP3_PLAYER_STACK_SIZE, MALLOC_CAP_SPIRAM)); } if (mp3_player_tcb == nullptr) { mp3_player_tcb = static_cast( 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; } return ESP_OK; } extern "C" void SDCard_Stop_Play(void) { mp3_pause_requested = false; if (!mp3_player_task_handle) { return; } mp3_stop_requested = true; } extern "C" void SDCard_Pause_Play(void) { mp3_pause_requested = true; } extern "C" void SDCard_Resume_Play(void) { mp3_pause_requested = false; Application::GetInstance().Schedule([]() { Application::GetInstance().ApplyMp3CommandOnlyMic(); }); } extern "C" void SDCard_PreparePlaybackAndWait(void) { stop_afe_for_sd_and_wait(true); } extern "C" bool SDCard_Mp3OfflineWakeAllowed(void) { return s_mp3_offline_wake_ok; } extern "C" bool SDCard_Is_Playing(void) { return mp3_player_task_handle != nullptr; } extern "C" bool SDCard_Is_Paused(void) { return mp3_player_task_handle != nullptr && mp3_pause_requested; } extern "C" int SDCard_Get_Repeat_Left(void) { return mp3_repeat_left; }