| | |
| | | #include <cstring> |
| | | #include <memory> |
| | | #include <esp_log.h> |
| | | #include <esp_heap_caps.h> |
| | | #include <cJSON.h> |
| | | #include <driver/gpio.h> |
| | | #include <arpa/inet.h> |
| | |
| | | |
| | | extern "C" void SDCard_Pause_Play(void) __attribute__((weak)); |
| | | extern "C" void SDCard_Resume_Play(void) __attribute__((weak)); |
| | | extern "C" void SDCard_Stop_Play(void) __attribute__((weak)); |
| | | extern "C" bool SDCard_Is_Playing(void) __attribute__((weak)); |
| | | extern "C" bool SDCard_Is_Paused(void) __attribute__((weak)); |
| | | |
| | |
| | | return SDCard_Is_Paused && SDCard_Is_Paused(); |
| | | } |
| | | |
| | | static bool ShouldHoldIdleForLessonWork() { |
| | | return Board_IsLessonSyncing() || Board_IsRepeaterTask() || Board_IsShadowingTask() || |
| | | Board_IsWaitingStartPhrase(); |
| | | } |
| | | |
| | | static bool IsLessonDetectText(const std::string& text) { |
| | | return text.rfind("[lesson_sync]", 0) == 0 || |
| | | text.rfind("[app_task]english_learning|", 0) == 0; |
| | | } |
| | | |
| | | static void PauseSdCardMp3() { |
| | | if (SDCard_Pause_Play) { |
| | | SDCard_Pause_Play(); |
| | |
| | | static void ResumeSdCardMp3() { |
| | | if (SDCard_Resume_Play) { |
| | | SDCard_Resume_Play(); |
| | | } |
| | | } |
| | | |
| | | static void StopSdCardMp3() { |
| | | if (SDCard_Stop_Play) { |
| | | SDCard_Stop_Play(); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | void Application::Run() { |
| | | main_loop_task_ = xTaskGetCurrentTaskHandle(); |
| | | // Set the priority of the main task to 10 |
| | | vTaskPrioritySet(nullptr, 10); |
| | | |
| | |
| | | display->UpdateStatusBar(); |
| | | auto state = GetDeviceState(); |
| | | if (state == kDeviceStateConnecting || state == kDeviceStateSpeaking || |
| | | state == kDeviceStateActivating || |
| | | (state == kDeviceStateIdle && suppress_wake_until_listen_)) { |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | } |
| | |
| | | if (greeting_listen_wait_ticks_ > 0) { |
| | | greeting_listen_wait_ticks_--; |
| | | if (greeting_listen_wait_ticks_ == 0) { |
| | | ESP_LOGW(TAG, "greeting TTS missing, start listening"); |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | if (ShouldHoldIdleForLessonWork()) { |
| | | ESP_LOGI(TAG, "skip greeting listen while lesson task"); |
| | | } else { |
| | | ESP_LOGW(TAG, "greeting TTS missing, start listening"); |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | } |
| | | } |
| | | } |
| | | } else if (Board_IsLessonSyncing()) { |
| | | boot_wake_restore_ticks_ = 0; |
| | | } else if (++boot_wake_restore_ticks_ >= 20) { |
| | | ESP_LOGW(TAG, "boot websocket not opened, restore wake word"); |
| | | suppress_wake_until_listen_ = false; |
| | |
| | | speaking_started_us_ > 0) { |
| | | const int64_t wait_ms = (esp_timer_get_time() - speaking_started_us_) / 1000; |
| | | if (wait_ms >= 1000 && (wait_ms / 1000) <= 8) { |
| | | ESP_LOGW(TAG, "TTS audio still missing after %lldms", (long long)wait_ms); |
| | | ESP_LOGW(TAG, "TTS audio still missing after %dms", (int)wait_ms); |
| | | } |
| | | } |
| | | if (protocol_ && IsSdCardMp3Playing() && protocol_->IsAudioChannelConnected()) { |
| | |
| | | pending_detect_text_.clear(); |
| | | } |
| | | } |
| | | if (hold_idle_for_prompt_ && !pending_detect_text_.empty() && |
| | | GetDeviceState() == kDeviceStateIdle && |
| | | protocol_ && protocol_->IsAudioChannelConnected()) { |
| | | if (pending_detect_retry_ticks_ > 0) { |
| | | pending_detect_retry_ticks_--; |
| | | } else if (pending_detect_retries_left_ > 0) { |
| | | pending_detect_retries_left_--; |
| | | pending_detect_retry_ticks_ = 2; |
| | | ESP_LOGW(TAG, "retry app_task detect: %s left=%d", |
| | | pending_detect_text_.c_str(), pending_detect_retries_left_); |
| | | SendListenDetectNow(pending_detect_text_); |
| | | } else if (IsLessonDetectText(pending_detect_text_) || ShouldHoldIdleForLessonWork()) { |
| | | ESP_LOGW(TAG, "lesson detect unanswered, keep idle: %s", |
| | | pending_detect_text_.c_str()); |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | } else { |
| | | ESP_LOGW(TAG, "app_task detect unanswered, start listening"); |
| | | hold_idle_for_prompt_ = false; |
| | | suppress_wake_until_listen_ = false; |
| | | pending_detect_text_.clear(); |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | } |
| | | } |
| | | |
| | | Board_PollPendingLessonPlayback(); |
| | | // Print debug info every 10 seconds |
| | | if (clock_ticks_ % 10 == 0) { |
| | | SystemInfo::PrintHeapStats(); |
| | |
| | | if (GetDeviceState() == kDeviceStateIdle) { |
| | | break; |
| | | } |
| | | // 激活请求失败时再播一遍 6 位码,避免只在开机瞬间播一次被听漏。 |
| | | if (ota_->HasActivationCode() && i < 9) { |
| | | ShowActivationCode(ota_->GetActivationCode(), ota_->GetActivationMessage()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | if (state == kDeviceStateListening || state == kDeviceStateIdle) { |
| | | tts_audio_expected_ = true; |
| | | if (!ShouldHoldIdleForLessonWork()) { |
| | | hold_idle_for_prompt_ = false; |
| | | } |
| | | } |
| | | |
| | | tts_response_started_ = true; |
| | | ESP_LOGI(TAG, "TTS audio packet bytes=%u expected=%d ready=%d state=%d", |
| | | ESP_LOGD(TAG, "TTS audio packet bytes=%u expected=%d ready=%d state=%d", |
| | | (unsigned)shared_packet->payload.size(), |
| | | tts_audio_expected_ ? 1 : 0, |
| | | tts_speaking_state_ready_ ? 1 : 0, |
| | |
| | | ESP_LOGI(TAG, "Skip channel-closed teardown while opening/reusing websocket"); |
| | | return; |
| | | } |
| | | // 跟读/复读还在播时不要切 LOW_POWER:后面补发 playlist_completed 会 handshake timeout。 |
| | | if ((Board_IsShadowingTask() || Board_IsRepeaterTask()) && IsSdCardMp3Playing()) { |
| | | board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | } else { |
| | | board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); |
| | | } |
| | | // 通道关掉后会立刻切 idle、加载唤醒词。这时再进 MAX_MODEM, |
| | | // MQTT 重连任务和 phy_track_pll 会把内部 SRAM 打穿,ESP_ERROR_CHECK 直接重启。 |
| | | // 保持 PERFORMANCE,等 PowerSaveTimer 超时后再真正断网待命。 |
| | | board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | Schedule([this]() { |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | opening_audio_channel_ = false; |
| | |
| | | tts_stop_pending_ = false; |
| | | tts_stop_waiting_ = false; |
| | | tts_idle_waiting_ = false; |
| | | tts_session_end_ = false; |
| | | tts_speaking_state_ready_ = false; |
| | | tts_activity_generation_++; |
| | | audio_service_.ResetDecoder(); |
| | |
| | | if (strcmp(state->valuestring, "start") == 0) { |
| | | Schedule([this]() { |
| | | aborted_ = false; |
| | | if (!ShouldHoldIdleForLessonWork()) { |
| | | hold_idle_for_prompt_ = false; |
| | | } |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | tts_audio_expected_ = true; |
| | | tts_response_started_ = false; |
| | | tts_audio_received_ = false; |
| | | tts_stop_pending_ = false; |
| | | tts_stop_waiting_ = false; |
| | | tts_session_end_ = false; |
| | | tts_playback_empty_ticks_ = 0; |
| | | tts_stop_wait_ticks_ = 0; |
| | | tts_activity_generation_++; |
| | |
| | | Board::GetInstance().GetAudioCodec()->output_enabled() ? 1 : 0); |
| | | }); |
| | | } else if (strcmp(state->valuestring, "stop") == 0) { |
| | | Schedule([this]() { |
| | | ESP_LOGI(TAG, "TTS stop started=%d received=%d", |
| | | tts_response_started_, tts_audio_received_); |
| | | auto session_end_item = cJSON_GetObjectItem(root, "session_end"); |
| | | const bool session_end = cJSON_IsTrue(session_end_item) || |
| | | (cJSON_IsNumber(session_end_item) && session_end_item->valuedouble != 0); |
| | | Schedule([this, session_end]() { |
| | | ESP_LOGI(TAG, "TTS stop started=%d received=%d session_end=%d", |
| | | tts_response_started_, tts_audio_received_, session_end ? 1 : 0); |
| | | if (session_end) { |
| | | tts_session_end_ = true; |
| | | } |
| | | tts_stop_pending_ = true; |
| | | FinishTtsResponseAfterPlayback(); |
| | | }); |
| | |
| | | return; |
| | | } |
| | | Schedule([this, display, message = std::string(text->valuestring)]() { |
| | | if (!ShouldHoldIdleForLessonWork()) { |
| | | hold_idle_for_prompt_ = false; |
| | | } |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | tts_audio_expected_ = true; |
| | | tts_response_started_ = true; |
| | | tts_activity_generation_++; |
| | | tts_playback_empty_ticks_ = 0; |
| | | audio_service_.SetAudioPowerInhibit(true); |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) { |
| | | if (Board_OnStartPhraseSentence(message.c_str())) { |
| | | resume_sdcard_mp3_after_tts_ = false; |
| | | resume_sdcard_mp3_after_voice_ = false; |
| | | } else if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) { |
| | | PauseSdCardMp3(); |
| | | resume_sdcard_mp3_after_tts_ = true; |
| | | } |
| | |
| | | protocol_->Start(); |
| | | } |
| | | |
| | | static void WaitUntilActivationPlaybackIdle(AudioService& audio) { |
| | | audio.KeepCodecReadyForPlayback(); |
| | | for (int i = 0; i < 150; ++i) { |
| | | if (audio.IsPlaybackQueueEmpty()) { |
| | | vTaskDelay(pdMS_TO_TICKS(150)); |
| | | return; |
| | | } |
| | | audio.KeepCodecReadyForPlayback(); |
| | | vTaskDelay(pdMS_TO_TICKS(100)); |
| | | } |
| | | } |
| | | |
| | | void Application::ShowActivationCode(const std::string& code, const std::string& message) { |
| | | struct digit_sound { |
| | | char digit; |
| | |
| | | digit_sound{'9', Lang::Sounds::OGG_9} |
| | | }}; |
| | | |
| | | ESP_LOGI(TAG, "Speaking activation code: %s", code.c_str()); |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | // 功放刚打开时前几百毫秒常是静音,先稳住再播,避免只听到提示音听不到数字。 |
| | | vTaskDelay(pdMS_TO_TICKS(400)); |
| | | |
| | | // This sentence uses 9KB of SRAM, so we need to wait for it to finish |
| | | Alert(Lang::Strings::ACTIVATION, message.c_str(), "link", Lang::Sounds::OGG_ACTIVATION); |
| | | WaitUntilActivationPlaybackIdle(audio_service_); |
| | | |
| | | for (const auto& digit : code) { |
| | | auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(), |
| | | [digit](const digit_sound& ds) { return ds.digit == digit; }); |
| | | if (it != digit_sounds.end()) { |
| | | ESP_LOGI(TAG, "Playing activation digit: %c", digit); |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | audio_service_.PlaySound(it->sound); |
| | | WaitUntilActivationPlaybackIdle(audio_service_); |
| | | vTaskDelay(pdMS_TO_TICKS(250)); |
| | | } |
| | | } |
| | | } |
| | |
| | | return protocol_->SendWakeWordDetected(text); |
| | | } |
| | | |
| | | void Application::HoldIdleForServerPrompt() { |
| | | hold_idle_for_prompt_ = true; |
| | | suppress_wake_until_listen_ = true; |
| | | auto state = GetDeviceState(); |
| | | if (state == kDeviceStateListening) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | } |
| | | |
| | | void Application::ReleaseHoldIdle() { |
| | | hold_idle_for_prompt_ = false; |
| | | suppress_wake_until_listen_ = false; |
| | | boot_wake_restore_ticks_ = 0; |
| | | } |
| | | |
| | | void Application::StartListeningAfterPrompt() { |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | } |
| | | |
| | | void Application::ArmPendingDetectRetry(const std::string& text, int retries) { |
| | | if (text.empty() || retries <= 0) { |
| | | return; |
| | | } |
| | | pending_detect_text_ = text; |
| | | pending_detect_retries_left_ = retries; |
| | | pending_detect_retry_ticks_ = 2; |
| | | } |
| | | |
| | | bool Application::IsAudioChannelConnected() const { |
| | | return protocol_ && protocol_->IsAudioChannelConnected(); |
| | | } |
| | | |
| | | void Application::AnnounceReturnedToChat() { |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | suppress_wake_until_listen_ = false; |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | display->SetStatus("对话模式"); |
| | | display->SetEmotion("happy"); |
| | | display->SetChatMessage("assistant", "我们来聊天吧。"); |
| | | audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS); |
| | | ESP_LOGI(TAG, "announced local return to chat"); |
| | | } |
| | | |
| | | void Application::ReconnectAfterLesson(const std::string& detect) { |
| | | // 课文结束 WS 已断:先释放 AFE,再握手补发 skill_event。 |
| | | // Server 会播「泛听/跟读结束。我们来聊天吧。」说完再 listening。 |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | suppress_wake_until_listen_ = true; |
| | | audio_service_.SetAudioPowerInhibit(false); |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | audio_service_.ReleaseAfeRuntime(); |
| | | |
| | | auto state = GetDeviceState(); |
| | | if (state == kDeviceStateListening || state == kDeviceStateSpeaking) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | |
| | | if (detect.empty() || !protocol_) { |
| | | ESP_LOGW(TAG, "lesson reconnect skipped, stay local chat"); |
| | | AnnounceReturnedToChat(); |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | return; |
| | | } |
| | | |
| | | // 泛听时长会把 WiFi 待机关掉(跟读有听窗所以不会)。不要用 IsNetworkReady() |
| | | // 直接放弃:后面 OpenAudioChannel 会 EnsureNetworkReadyForWake,连上再播结束语。 |
| | | ESP_LOGI(TAG, "lesson reconnect for %s", detect.c_str()); |
| | | Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | SetDeviceState(kDeviceStateConnecting); |
| | | Schedule([this, detect]() { |
| | | ContinueOpenAudioChannelForDetect(detect); |
| | | }); |
| | | } |
| | | |
| | | void Application::SendListenDetect(const std::string& text, bool open_if_idle) { |
| | | Schedule([this, text, open_if_idle]() { |
| | | if (!protocol_ || text.empty()) { |
| | |
| | | Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | protocol_->MarkIncomingActivity(); |
| | | if (protocol_->SendWakeWordDetected(text)) { |
| | | Board_NoteLessonStartDetectSent(); |
| | | return; |
| | | } |
| | | ESP_LOGW(TAG, "detect send failed, reopen audio channel"); |
| | |
| | | // AFE 起来后内部堆只剩几 KB,xTaskCreate(8KB) 会直接失败。 |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | const size_t dma_before = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); |
| | | ESP_LOGI(TAG, "OpenAudioChannel DMA largest=%u free=%u", |
| | | (unsigned)dma_before, |
| | | (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); |
| | | if (dma_before < 8192) { |
| | | ESP_LOGW(TAG, "low DMA, release AFE before websocket"); |
| | | audio_service_.ClearWakeWordBuffers(); |
| | | audio_service_.ReleaseAfeRuntime(); |
| | | vTaskDelay(pdMS_TO_TICKS(50)); |
| | | ESP_LOGI(TAG, "OpenAudioChannel DMA after AFE release=%u", |
| | | (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA)); |
| | | } |
| | | Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | audio_service_.SetAudioPowerInhibit(true); |
| | | |
| | |
| | | listening_mode_ = GetDefaultListeningMode(); |
| | | suppress_wake_until_listen_ = true; |
| | | if (!OpenAudioChannelWithRetry(2)) { |
| | | pending_detect_text_ = detect_text; |
| | | if (pending_detect_retries_left_ <= 0) { |
| | | pending_detect_retries_left_ = 3; |
| | | // 课文结束后通道已死时不要反复 handshake:会卡在「连接中」并关掉唤醒词。 |
| | | const bool skip_retry = detect_text.rfind("[skill_event]", 0) == 0; |
| | | if (skip_retry) { |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | } else { |
| | | pending_detect_text_ = detect_text; |
| | | if (pending_detect_retries_left_ <= 0) { |
| | | pending_detect_retries_left_ = 3; |
| | | } |
| | | pending_detect_retry_ticks_ = 3; |
| | | } |
| | | pending_detect_retry_ticks_ = 3; |
| | | suppress_wake_until_listen_ = false; |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | SetDeviceState(kDeviceStateIdle); |
| | | ESP_LOGW(TAG, "boot websocket failed, will retry detect in 3s"); |
| | | if (skip_retry) { |
| | | ESP_LOGW(TAG, "websocket failed after lesson, keep idle chat without retry"); |
| | | AnnounceReturnedToChat(); |
| | | } else { |
| | | ESP_LOGW(TAG, "boot websocket failed, will retry detect in 3s"); |
| | | } |
| | | if (IsLessonDetectText(detect_text) || ShouldHoldIdleForLessonWork()) { |
| | | suppress_wake_until_listen_ = true; |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | } else { |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | greeting_listen_wait_ticks_ = 8; |
| | | greeting_listen_wait_ticks_ = (IsLessonDetectText(detect_text) || ShouldHoldIdleForLessonWork()) ? 0 : 8; |
| | | if (!detect_text.empty()) { |
| | | protocol_->MarkIncomingActivity(); |
| | | protocol_->SendWakeWordDetected(detect_text); |
| | | if (protocol_->SendWakeWordDetected(detect_text)) { |
| | | Board_NoteLessonStartDetectSent(); |
| | | } |
| | | } |
| | | // 保持待命且不开 AFE:欢迎词从 idle 进 speaking。说完再 listening 开麦。 |
| | | SetDeviceState(kDeviceStateIdle); |
| | |
| | | if (!IsSdCardMp3Playing() || IsSdCardMp3Paused()) { |
| | | return; |
| | | } |
| | | // 音频在播:不上行录音/闲聊,只留离线口令(退出复读/跟读、我们聊天吧)。 |
| | | // 音频在播:不上行录音/闲聊,只留离线口令(退出泛听/跟读、我们聊天吧)。 |
| | | // 课文还在读 SD 时禁止拉起 AFE:会把 SDMMC DMA 需要的连续内部 SRAM 吃光,fopen abort。 |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | const size_t largest_internal = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); |
| | | if (!SDCard_Mp3OfflineWakeAllowed() || largest_internal < 32768) { |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | if (!SDCard_Mp3OfflineWakeAllowed()) { |
| | | ESP_LOGD(TAG, "offline wake deferred until MP3 cached"); |
| | | } else { |
| | | ESP_LOGW(TAG, "skip offline wake, internal largest=%u", (unsigned)largest_internal); |
| | | } |
| | | } else { |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | } |
| | | if (GetDeviceState() != kDeviceStateListening) { |
| | | return; |
| | | } |
| | |
| | | void Application::CloseShadowingListenWindow() |
| | | { |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | // 下一句还要 fopen/解码,「跟读结束」TTS 还要 mbedtls AES。这里不要立刻拉起 |
| | | // AFE 唤醒词,否则内部 SRAM 只剩 1KB 连续块,SD DMA / SSL 都会失败。 |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | if (protocol_ && protocol_->IsAudioChannelOpened()) { |
| | | while (auto packet = audio_service_.PopPacketFromSendQueue()) { |
| | | if (!protocol_->SendAudio(std::move(packet))) { |
| | |
| | | |
| | | if (state == kDeviceStateIdle) { |
| | | PauseSdCardMp3ForVoice(); |
| | | audio_service_.EncodeWakeWord(); |
| | | size_t dma_largest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); |
| | | // 再见后唤醒词 AFE 还占着内部堆,DMA 往往不够编码 opus。 |
| | | // 旧逻辑会跳过编码却继续阻塞等 opus,主循环卡死,15s 后 codec 掉电,像软关机。 |
| | | if (dma_largest < 12288) { |
| | | ESP_LOGW(TAG, "low DMA before wake handshake, release AFE largest=%u", |
| | | (unsigned)dma_largest); |
| | | audio_service_.ReleaseAfeRuntime(); |
| | | dma_largest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); |
| | | } |
| | | if (dma_largest >= 8192) { |
| | | audio_service_.EncodeWakeWord(); |
| | | } else { |
| | | ESP_LOGW(TAG, "skip wake opus encode, DMA largest=%u", (unsigned)dma_largest); |
| | | audio_service_.AbortWakeWordEncode(); |
| | | } |
| | | auto wake_word = audio_service_.GetLastWakeWord(); |
| | | |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | |
| | | tts_stop_pending_ = false; |
| | | tts_stop_waiting_ = false; |
| | | tts_idle_waiting_ = false; |
| | | tts_session_end_ = false; |
| | | tts_speaking_state_ready_ = false; |
| | | tts_activity_generation_++; |
| | | } |
| | |
| | | display->ClearChatMessages(); // Clear messages first |
| | | display->SetEmotion("neutral"); // Then set emotion (wechat mode checks child count) |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | if (suppress_wake_until_listen_) { |
| | | // 再见断线会 speaking→listening→idle:processor AFE 没释放就把唤醒词 AFE 拉起来, |
| | | // audio_detection 建不起来,feed 刷屏直到内部 SRAM 打穿。 |
| | | if (!suppress_wake_until_listen_) { |
| | | audio_service_.ReleaseVoiceProcessorRuntime(); |
| | | } |
| | | if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) { |
| | | ApplyMp3CommandOnlyMic(); |
| | | } else if (suppress_wake_until_listen_ || Board_IsLessonSyncing() || |
| | | Board_IsRepeaterTask() || Board_IsShadowingTask()) { |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | audio_service_.SetAudioPowerInhibit(true); |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | } else if (Board_IsRepeaterTask()) { |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | audio_service_.SetAudioPowerInhibit(false); |
| | | } else if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) { |
| | | ApplyMp3CommandOnlyMic(); |
| | | } else { |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | audio_service_.SetAudioPowerInhibit(false); |
| | |
| | | // Do nothing |
| | | break; |
| | | } |
| | | } |
| | | |
| | | bool Application::IsOnMainLoop() const { |
| | | return main_loop_task_ != nullptr && xTaskGetCurrentTaskHandle() == main_loop_task_; |
| | | } |
| | | |
| | | void Application::Schedule(std::function<void()>&& callback) { |
| | |
| | | tts_stop_pending_ = false; |
| | | tts_stop_waiting_ = false; |
| | | tts_idle_waiting_ = false; |
| | | tts_session_end_ = false; |
| | | tts_speaking_state_ready_ = false; |
| | | tts_activity_generation_++; |
| | | |
| | |
| | | } |
| | | |
| | | void Application::FinishTtsResponse() { |
| | | const bool end_session = tts_session_end_; |
| | | const bool hold_idle = hold_idle_for_prompt_; |
| | | tts_audio_expected_ = false; |
| | | tts_response_started_ = false; |
| | | tts_audio_received_ = false; |
| | | tts_stop_pending_ = false; |
| | | tts_stop_waiting_ = false; |
| | | tts_idle_waiting_ = false; |
| | | tts_session_end_ = false; |
| | | tts_speaking_state_ready_ = false; |
| | | tts_playback_empty_ticks_ = 0; |
| | | tts_stop_wait_ticks_ = 0; |
| | | speaking_started_us_ = 0; |
| | | audio_service_.SetAudioPowerInhibit(false); |
| | | suppress_wake_until_listen_ = false; |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | if (hold_idle || ShouldHoldIdleForLessonWork()) { |
| | | suppress_wake_until_listen_ = true; |
| | | } else { |
| | | suppress_wake_until_listen_ = false; |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | } |
| | | ResumeSdCardMp3AfterVoice(); |
| | | if (resume_sdcard_mp3_after_tts_ && !sdcard_mp3_stop_requested_) { |
| | | resume_sdcard_mp3_after_tts_ = false; |
| | |
| | | resume_sdcard_mp3_after_tts_ = false; |
| | | } |
| | | if (GetDeviceState() == kDeviceStateSpeaking) { |
| | | if (listening_mode_ == kListeningModeManualStop || |
| | | if (end_session || hold_idle || ShouldHoldIdleForLessonWork() || |
| | | listening_mode_ == kListeningModeManualStop || |
| | | (IsSdCardMp3Playing() && !IsSdCardMp3Paused())) { |
| | | ESP_LOGI(TAG, "TTS done, return to idle session_end=%d hold=%d lesson=%d", |
| | | end_session ? 1 : 0, hold_idle ? 1 : 0, |
| | | ShouldHoldIdleForLessonWork() ? 1 : 0); |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } else { |
| | | listening_mode_ = GetDefaultListeningMode(); |
| | |
| | | if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) { |
| | | ApplyMp3CommandOnlyMic(); |
| | | } |
| | | Board_FlushPendingLessonPlayback(); |
| | | if (power_off_requested_) { |
| | | power_off_requested_ = false; |
| | | Board_Enter_Power_Off(); |
| | |
| | | tts_stop_wait_ticks_++; |
| | | const int empty_needed = tts_audio_received_ ? 1 : 2; |
| | | if (playback_empty && tts_playback_empty_ticks_ >= empty_needed) { |
| | | ESP_LOGI(TAG, "TTS done, return to listen empty=%d wait=%d received=%d", |
| | | tts_playback_empty_ticks_, tts_stop_wait_ticks_, tts_audio_received_); |
| | | ESP_LOGI(TAG, "TTS done empty=%d wait=%d received=%d session_end=%d", |
| | | tts_playback_empty_ticks_, tts_stop_wait_ticks_, tts_audio_received_, |
| | | tts_session_end_ ? 1 : 0); |
| | | FinishTtsResponse(); |
| | | } else if (!playback_empty && tts_stop_wait_ticks_ >= 30) { |
| | | ESP_LOGW(TAG, "TTS playback stuck after stop, return to listen"); |
| | |
| | | |
| | | // 欢迎词/长回答:stop 没到时,只在「一直没收到 opus」或「播完后长时间空队列」才回听。 |
| | | // 不要用说话墙钟 15s/队列空 2s:中英对话长句会在句间空隙或 15 秒处被掐断。 |
| | | if (!tts_audio_received_ && elapsed_ms >= 5000) { |
| | | ESP_LOGW(TAG, "TTS stop missing, return to listen elapsed=%lldms received=0 empty=%d", |
| | | (long long)elapsed_ms, tts_playback_empty_ticks_); |
| | | if (!tts_audio_received_ && elapsed_ms >= 10000) { |
| | | ESP_LOGW(TAG, "TTS stop missing, return to listen elapsed=%dms received=0 empty=%d", |
| | | (int)elapsed_ms, tts_playback_empty_ticks_); |
| | | FinishTtsResponse(); |
| | | } else if (playback_empty && tts_audio_received_ && tts_playback_empty_ticks_ >= 8) { |
| | | ESP_LOGW(TAG, "TTS stop missing after idle playback elapsed=%lldms empty=%d", |
| | | (long long)elapsed_ms, tts_playback_empty_ticks_); |
| | | ESP_LOGW(TAG, "TTS stop missing after idle playback elapsed=%dms empty=%d", |
| | | (int)elapsed_ms, tts_playback_empty_ticks_); |
| | | FinishTtsResponse(); |
| | | } |
| | | } |
| | |
| | | return false; |
| | | } |
| | | |
| | | // 泛听是 idle 播 SD MP3,AudioService 队列是空的,会被当成可休眠。 |
| | | // 跟读每句有听窗所以不会进 WiFi 待机;泛听会把网关掉,结束语就发不出去。 |
| | | if (IsSdCardMp3Playing() || Board_IsRepeaterTask() || Board_IsShadowingTask()) { |
| | | return false; |
| | | } |
| | | |
| | | if (!audio_service_.IsIdle()) { |
| | | return false; |
| | | } |