| | |
| | | |
| | | typedef struct { |
| | | int count; |
| | | sdcard_mp3_playlist_item_t items[SDCARD_MP3_PLAYLIST_MAX]; |
| | | sdcard_mp3_playlist_item_t items[]; |
| | | } mp3_player_task_arg_t; |
| | | |
| | | static TaskHandle_t mp3_player_task_handle; |
| | |
| | | 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; |
| | |
| | | return !mp3_stop_requested; |
| | | } |
| | | |
| | | static void wait_for_sd_dma(void) |
| | | static bool wait_for_internal_sram(size_t need_bytes, int tries) |
| | | { |
| | | for (int i = 0; i < 20; ++i) { |
| | | for (int i = 0; i < tries; ++i) { |
| | | size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); |
| | | if (largest >= 4096) { |
| | | return; |
| | | if (largest >= need_bytes) { |
| | | return true; |
| | | } |
| | | ESP_LOGW(TAG, "wait SD DMA largest=%u try=%d", (unsigned)largest, i); |
| | | 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; |
| | | wait_for_sd_dma(); |
| | | FILE *fp = fopen(path, "rb"); |
| | | FILE *fp = fopen_mp3_if_heap_ok(path); |
| | | if (!fp) { |
| | | return nullptr; |
| | | } |
| | |
| | | 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); |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | |
| | | { |
| | | mp3_player_task_handle = xTaskGetCurrentTaskHandle(); |
| | | mp3_player_task_arg_t *task_arg = static_cast<mp3_player_task_arg_t *>(arg); |
| | | sdcard_mp3_playlist_item_t items[SDCARD_MP3_PLAYLIST_MAX]; |
| | | int count = task_arg->count; |
| | | int count = task_arg ? task_arg->count : 0; |
| | | 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); |
| | | 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) { |
| | | 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_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; |
| | | if (item_i > 0) { |
| | | pause_wake_for_sd(); |
| | | } |
| | | 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) { |
| | | wait_for_sd_dma(); |
| | | fp = fopen(path, "rb"); |
| | | fp = fopen_mp3_if_heap_ok(path); |
| | | } |
| | | if (!file_buf && !fp) { |
| | | ESP_LOGE(TAG, "open MP3 failed: %s", path); |
| | | continue; |
| | | } |
| | | |
| | | if (!mp3_pause_requested) { |
| | | Application::GetInstance().Schedule([]() { |
| | | Application::GetInstance().ApplyMp3CommandOnlyMic(); |
| | | }); |
| | | // 先把文件读进 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; |
| | |
| | | fp = nullptr; |
| | | } |
| | | vTaskDelay(pdMS_TO_TICKS(30)); |
| | | wait_for_sd_dma(); |
| | | fp = fopen(path, "rb"); |
| | | fp = fopen_mp3_if_heap_ok(path); |
| | | if (!fp) { |
| | | ESP_LOGE(TAG, "reopen MP3 failed: %s", path); |
| | | break; |
| | |
| | | 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(); |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | close_mp3_decode_resources(&decoder, &rate_cvt_handle, &fp, &file_buf); |
| | | ESP_LOGI(TAG, "MP3 item %s: %s", mp3_stop_requested ? "stopped" : "finished", path); |
| | | } |
| | | |
| | |
| | | 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(); |
| | | } |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | 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)); |
| | | heap_caps_calloc(1, |
| | | sizeof(mp3_player_task_arg_t) + |
| | | sizeof(sdcard_mp3_playlist_item_t) * static_cast<size_t>(count), |
| | | MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); |
| | | if (!arg) { |
| | | return ESP_ERR_NO_MEM; |
| | | } |
| | |
| | | }); |
| | | } |
| | | |
| | | 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; |