fix: 软 RTC 退出待命音三连修 + 尾音延迟
问题与修复(按发现顺序): 【问题1】AudioLoop guard 冻结 OnAudioOutput → 待命音队列永远不被消费 - 现象: WaitForAudioPlayback 3秒超时, 无声 - 根因: EnterIdleHibernate Step 0 设 hibernating_=true, AudioLoop 顶层 if(hibernating_) continue 会同时跳过 OnAudioInput + OnAudioOutput, PlaySound 入队的 Opus 永远不解码。 - 修复: guard 下沉到 OnAudioInput 入口(仅 input 侧关 codec 有 bad_alloc 风险), OnAudioOutput 自带 codec->output_enabled() 保护。 【问题2】volc_rtc_stop 副作用关 I2S 通道 → codec 状态错位 - 现象: 听到杂音而非待命音; i2s_channel_disable "not enabled yet" 错误 - 根因: 火山 RTC SDK 的 stop 内部关闭 ES8311 I2S, 但 codec class 内部 output_enabled_ 标志仍是 true → 状态错位, PlaySound 写入到 disabled 的 I2S。 - 修复: EnterIdleHibernate 在 PlaySound 前显式 EnableOutput(false→true) 强制重新激活 I2S, 并灌 200ms silence 覆盖 DMA 残留。 【问题3 - 真因】protocol downlink_is_pcm_ 标志位污染 → Opus 被当 PCM 字节流写出 - 现象: 杂音仍在 - 根因: 火山 RTC 下行音频是 PCM, DataCallback 设 downlink_is_pcm_=true。 LeaveRoom 没重置这个 flag, 后续 hibernate 中 PlaySound 入队的 Opus 包, OnAudioOutput 读到 protocol_->downlink_is_pcm() 返回 true → treat_as_pcm=true → 跳过 opus_decoder, 直接把 Opus 编码字节当 int16 PCM 样本写到 codec → 杂音。 - 修复: VolcRtcProtocol::LeaveRoom 末尾重置 downlink_is_pcm_=false + first_downlink_logged_=false。唤醒重连后 DataCallback 收到首包会立即 重新设置该 flag, 不影响欢迎语 PCM 播放。 【问题4】WaitForAudioPlayback 完成 ≠ DMA 输出完成 → 尾音被截 - 现象: 待命音能听见但提前结束约 1 秒 - 根因: WaitForAudioPlayback 只判断 audio_decode_queue_ 出队完毕。 OnAudioOutput 是 background_task Schedule 异步执行 codec write, 队列空 ≠ codec 写完; codec.Write 返回 ≠ I2S DMA + ES8311 FIFO 输出完毕。 - 修复: WaitForAudioPlayback 之后追加 background_task->WaitForCompletion + vTaskDelay(1000) 让 DMA 尾音自然衰减, 才关 player_pipeline。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a3a476f857
commit
b1577d8418
@ -1966,11 +1966,10 @@ void Application::MainLoop() {
|
|||||||
void Application::AudioLoop() {
|
void Application::AudioLoop() {
|
||||||
auto codec = Board::GetInstance().GetAudioCodec();
|
auto codec = Board::GetInstance().GetAudioCodec();
|
||||||
while (true) {
|
while (true) {
|
||||||
// Phase 6 移植:hibernate 期间跳过 codec 访问,避免与 EnterIdleHibernate 关闭 codec 时竞态
|
// Phase 6 修正(路径 A):原顶层 if(hibernating_) continue 会 freeze 整个循环,
|
||||||
if (hibernating_.load()) {
|
// 导致 PlaySound 入队的待命音 Opus 永远不被 OnAudioOutput 消费 → WaitForAudioPlayback 超时 → 无声。
|
||||||
vTaskDelay(pdMS_TO_TICKS(50));
|
// 修正:guard 下沉到 OnAudioInput 入口(input 侧关 codec 时才有 std::bad_alloc 风险),
|
||||||
continue;
|
// OnAudioOutput 自带 codec->output_enabled() 判断,hibernate 末尾关 output 后自然停。
|
||||||
}
|
|
||||||
OnAudioInput();
|
OnAudioInput();
|
||||||
if (codec->output_enabled()) {
|
if (codec->output_enabled()) {
|
||||||
OnAudioOutput();
|
OnAudioOutput();
|
||||||
@ -2228,10 +2227,10 @@ void Application::OnAudioOutput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Application::OnAudioInput() {
|
void Application::OnAudioInput() {
|
||||||
// Phase 6 移植:codec input 未启用或 recorder_pipeline 未就绪时跳过
|
// Phase 6 修正(路径 A):hibernate 期间禁用输入侧,但允许 OnAudioOutput 跑(消费待命音队列)。
|
||||||
// 防止 EnterIdleHibernate / WakeFromHibernate 过渡期间访问到关闭的 codec → std::bad_alloc abort
|
// 加 hibernating_ 覆盖 Step 0→Step 4 之间约 50ms 的窗口期(那时 codec input 还 enabled)。
|
||||||
auto codec_for_guard = Board::GetInstance().GetAudioCodec();
|
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));
|
vTaskDelay(pdMS_TO_TICKS(20));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -4380,6 +4379,23 @@ void Application::EnterIdleHibernate() {
|
|||||||
esp_pm_configure(&pm_config);
|
esp_pm_configure(&pm_config);
|
||||||
ESP_LOGI(TAG, "EnterIdleHibernate: 已强制禁用 Light Sleep(保护 I2C 总线)");
|
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<int16_t> silence(silence_samples, 0);
|
||||||
|
codec->OutputData(silence);
|
||||||
|
ESP_LOGI(TAG, "EnterIdleHibernate: 🔇 已灌 200ms 静音 PCM 覆盖 DMA 残留");
|
||||||
|
}
|
||||||
|
|
||||||
// 6. 设备状态切回 idle(SetDeviceState 内部触发 PlaySound 入队待命音 Opus)
|
// 6. 设备状态切回 idle(SetDeviceState 内部触发 PlaySound 入队待命音 Opus)
|
||||||
SetDeviceState(kDeviceStateIdle);
|
SetDeviceState(kDeviceStateIdle);
|
||||||
|
|
||||||
@ -4387,6 +4403,19 @@ void Application::EnterIdleHibernate() {
|
|||||||
ESP_LOGI(TAG, "EnterIdleHibernate: 等待待命音播放完成...");
|
ESP_LOGI(TAG, "EnterIdleHibernate: 等待待命音播放完成...");
|
||||||
WaitForAudioPlayback();
|
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 重开
|
// 8. 待命音播完后关 player_pipeline(内部会关 EnableOutput),WakeFromHibernate 重开
|
||||||
if (player_pipeline_) {
|
if (player_pipeline_) {
|
||||||
player_pipeline_close(player_pipeline_);
|
player_pipeline_close(player_pipeline_);
|
||||||
|
|||||||
@ -421,6 +421,15 @@ void VolcRtcProtocol::LeaveRoom(bool notify_closed) {
|
|||||||
ESP_LOGI(TAG, "✓ 已 stop RTC 房间(保留 handle 供唤醒复用, notify_closed=%d)", (int)notify_closed);
|
ESP_LOGI(TAG, "✓ 已 stop RTC 房间(保留 handle 供唤醒复用, notify_closed=%d)", (int)notify_closed);
|
||||||
}
|
}
|
||||||
is_audio_channel_opened_ = false;
|
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_) {
|
if (notify_closed && on_audio_channel_closed_) {
|
||||||
on_audio_channel_closed_();
|
on_audio_channel_closed_();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user