#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[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 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 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( 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 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(arg); 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(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 || 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( 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"); } 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 = { .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)); wait_for_sd_dma(); fp = fopen(path, "rb"); 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; 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); } 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); mp3_player_task_handle = nullptr; mp3_stop_requested = false; mp3_pause_requested = false; mp3_repeat_left = 0; 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) { 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), 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" 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; }