diff --git a/main/application.cc b/main/application.cc index 3b6fe8e..78ec05f 100644 --- a/main/application.cc +++ b/main/application.cc @@ -1966,11 +1966,10 @@ void Application::MainLoop() { void Application::AudioLoop() { auto codec = Board::GetInstance().GetAudioCodec(); while (true) { - // Phase 6 移植:hibernate 期间跳过 codec 访问,避免与 EnterIdleHibernate 关闭 codec 时竞态 - if (hibernating_.load()) { - vTaskDelay(pdMS_TO_TICKS(50)); - continue; - } + // Phase 6 修正(路径 A):原顶层 if(hibernating_) continue 会 freeze 整个循环, + // 导致 PlaySound 入队的待命音 Opus 永远不被 OnAudioOutput 消费 → WaitForAudioPlayback 超时 → 无声。 + // 修正:guard 下沉到 OnAudioInput 入口(input 侧关 codec 时才有 std::bad_alloc 风险), + // OnAudioOutput 自带 codec->output_enabled() 判断,hibernate 末尾关 output 后自然停。 OnAudioInput(); if (codec->output_enabled()) { OnAudioOutput(); @@ -2228,10 +2227,10 @@ void Application::OnAudioOutput() { } void Application::OnAudioInput() { - // Phase 6 移植:codec input 未启用或 recorder_pipeline 未就绪时跳过 - // 防止 EnterIdleHibernate / WakeFromHibernate 过渡期间访问到关闭的 codec → std::bad_alloc abort + // Phase 6 修正(路径 A):hibernate 期间禁用输入侧,但允许 OnAudioOutput 跑(消费待命音队列)。 + // 加 hibernating_ 覆盖 Step 0→Step 4 之间约 50ms 的窗口期(那时 codec input 还 enabled)。 auto codec_for_guard = Board::GetInstance().GetAudioCodec(); - if (!codec_for_guard || !codec_for_guard->input_enabled() || !recorder_pipeline_) { + if (hibernating_.load() || !codec_for_guard || !codec_for_guard->input_enabled() || !recorder_pipeline_) { vTaskDelay(pdMS_TO_TICKS(20)); return; } @@ -4380,6 +4379,23 @@ void Application::EnterIdleHibernate() { esp_pm_configure(&pm_config); ESP_LOGI(TAG, "EnterIdleHibernate: 已强制禁用 Light Sleep(保护 I2C 总线)"); + // 5.5 🆕 方向 1:强制重启 codec output 通道,清掉 LeaveRoom 副作用 + // volc_rtc_stop 内部会关闭 ES8311 I2S 通道(ESP-IDF i2s_channel_disable), + // 但 codec class 的 output_enabled_ 标志仍是 true → 后续 PlaySound 写入 + // 到的是 disabled 的 I2S → DMA 输出残留垃圾数据 → 杂音而非待命音。 + // 这里通过 disable→delay→enable 强制走 i2s_channel_enable 重新激活通道, + // 然后灌 200ms 静音 PCM 覆盖 DMA 残留(复用 OnAudioChannelOpened 经验)。 + if (codec) { + ESP_LOGI(TAG, "EnterIdleHibernate: 强制重启 codec output 通道(清 LeaveRoom 副作用)"); + codec->EnableOutput(false); + vTaskDelay(pdMS_TO_TICKS(20)); // 等 i2s_channel_disable 完成 + DMA 释放 + codec->EnableOutput(true); + const int silence_samples = codec->output_sample_rate() / 5; // 200ms + std::vector silence(silence_samples, 0); + codec->OutputData(silence); + ESP_LOGI(TAG, "EnterIdleHibernate: 🔇 已灌 200ms 静音 PCM 覆盖 DMA 残留"); + } + // 6. 设备状态切回 idle(SetDeviceState 内部触发 PlaySound 入队待命音 Opus) SetDeviceState(kDeviceStateIdle); @@ -4387,6 +4403,19 @@ void Application::EnterIdleHibernate() { ESP_LOGI(TAG, "EnterIdleHibernate: 等待待命音播放完成..."); WaitForAudioPlayback(); + // 7.5 🆕 等待背景任务清空 + DMA 尾音播完 + // WaitForAudioPlayback 只确认 audio_decode_queue_ 出队完毕,但 OnAudioOutput + // 是 Schedule 到 background_task 异步执行 codec write 的,队列空 ≠ codec 写完。 + // codec.Write 返回 ≠ I2S DMA / ES8311 内部 FIFO 输出到喇叭完成。 + // 所以这里两步保险:先 WaitForCompletion 等所有解码任务跑完,再 vTaskDelay + // 1000ms 让 DMA + 功放尾音自然衰减。否则 player_pipeline_close 立即停 I2S, + // 最后约 1 秒待命音会被截掉。 + if (background_task_) { + background_task_->WaitForCompletion(); + } + vTaskDelay(pdMS_TO_TICKS(1000)); + ESP_LOGI(TAG, "EnterIdleHibernate: DMA 尾音衰减完成"); + // 8. 待命音播完后关 player_pipeline(内部会关 EnableOutput),WakeFromHibernate 重开 if (player_pipeline_) { player_pipeline_close(player_pipeline_); diff --git a/main/protocols/volc_rtc_protocol.cc b/main/protocols/volc_rtc_protocol.cc index 3e258d2..a2cb3dc 100644 --- a/main/protocols/volc_rtc_protocol.cc +++ b/main/protocols/volc_rtc_protocol.cc @@ -421,6 +421,15 @@ void VolcRtcProtocol::LeaveRoom(bool notify_closed) { ESP_LOGI(TAG, "✓ 已 stop RTC 房间(保留 handle 供唤醒复用, notify_closed=%d)", (int)notify_closed); } is_audio_channel_opened_ = false; + + // 🔴 关键修复:重置下行音频格式标志位 + // 原因:RTC 下行是 PCM,DataCallback 把 downlink_is_pcm_=true。退房后若不重置, + // 后续 hibernate 中 PlaySound 入队的 Opus 包会被 OnAudioOutput 当成 raw PCM 写出去, + // → 杂音而非待命音。 + // 唤醒重连后 DataCallback 收到第一包又会立即重新置位(每包都更新),不影响欢迎语播放。 + downlink_is_pcm_ = false; + first_downlink_logged_ = false; + if (notify_closed && on_audio_channel_closed_) { on_audio_channel_closed_(); }