fix: 同步 Kapi 软 RTC 退出五连修到数字人项目(待命音 + 欢迎语杂音)
从 Kapi commit b1577d8 / a3a476f 完整移植 5 个修复,覆盖三类问题:
1. 开机/唤醒后按 BOOT 进 RTC 房间,欢迎语前 1-3 秒杂音
2. 软 RTC 退出(41s 无对话触发 Dialog watchdog)后待命音"卡卡正在待命"无声/杂音/被截
3. 软退出后按 BOOT 唤醒,欢迎语前杂音
【修复 1】OnAudioChannelOpened EnableOutput(true) 后立刻灌 200ms silence
- 防止 I2S DMA 启动后到 RTC 真实 PCM 到达 1-3s 空窗的杂音
【修复 2】LeaveRoom 加 notify_closed 参数(默认 true 不变老路径)
- hibernate 路径传 false 跳过 on_audio_channel_closed_ 回调
- 避免回调链 player_pipeline_close → EnableOutput(false) 误关 codec
导致待命音无声
【修复 3】LeaveRoom 不再 volc_rtc_destroy, 保留 rtc_handle_
- 唤醒时 OpenAudioChannel 直接 volc_rtc_start 复用 handle, 不死循环
- 服务端 AI 任务无需 destroy 也会按 180s 兜底机制清理
【修复 4 - 最隐蔽】LeaveRoom 末尾重置 downlink_is_pcm_ = false
- 火山 RTC 下行是 PCM, DataCallback 设 downlink_is_pcm_=true
- 不重置 → PlaySound 的 Opus 包被 OnAudioOutput 当成 raw PCM 字节流
直接写 codec → 杂音而非待命音
- 唤醒重连后 DataCallback 收下一包会自动重置, 不影响欢迎语
【修复 5】OnAudioInput 入口加 hibernating_ guard
- hibernate 期间禁用输入侧, 防止访问关闭的 codec → std::bad_alloc abort
- 不冻结 OnAudioOutput, 让待命音队列正常被消费
【EnterIdleHibernate 重写】套用 Kapi 新顺序:
Step 0: hibernating_=true + 50ms (让 OnAudioInput guard 生效)
Step 1: LeaveRoom(false) (codec output 保留)
Step 2: background_task->WaitForCompletion
Step 3: 清空 audio_decode_queue_
Step 4: EnableInput(false) + close recorder_pipeline
Step 5: 强制 esp_pm 禁用 Light Sleep
Step 5.5: EnableOutput(false→true) + 200ms silence (清 LeaveRoom 副作用)
Step 6: SetDeviceState(idle) → PlaySound 待命音
Step 7: WaitForAudioPlayback (队列消费完毕)
Step 7.5: background_task->WaitForCompletion + vTaskDelay(1000)
(DMA + ES8311 FIFO + 功放尾音衰减, 防尾音截断)
Step 8: player_pipeline_close
Step 9: NVS idle_cycles_++
Step 10: 显示字幕"已自动退出RTC对话..."(数字人特有, 保留)
【WakeFromHibernate】调整 hibernating_=false 顺序
- 先放下 hibernating_, 让 ToggleChatState 期间 OnAudioInput guard 通过
- 否则 ToggleChatState 期间音频上行迟迟不开
编译: kapi.bin 0x41c000 (4.21MB), 分区 25% 空闲。
实测三项全通: 欢迎语干净 + 待命音清晰完整 + 唤醒欢迎语干净。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eceadda807
commit
22b7a70d7d
@ -848,6 +848,18 @@ void Application::Start() {
|
|||||||
ESP_LOGI(TAG, "🔊 启用音频编解码器输出");
|
ESP_LOGI(TAG, "🔊 启用音频编解码器输出");
|
||||||
codec->EnableOutput(true);// 启用音频编解码器输出
|
codec->EnableOutput(true);// 启用音频编解码器输出
|
||||||
|
|
||||||
|
// 🆕 移植自 Kapi commit a3a476f: 灌 200ms 真实静音 PCM 覆盖 I2S DMA 残留数据
|
||||||
|
// 原因:EnableOutput 启动 I2S 后, RTC 真实 PCM 需 1-3 秒才到达
|
||||||
|
// 这段空窗期 I2S DMA 输出垃圾数据 → PA 放大 → 杂音
|
||||||
|
// 灌真实 0x0000 静音后, I2S DMA 输出真实 0V, PA 放大 = 完全无声
|
||||||
|
// 后续 RTC PCM 自然衔接, 无杂音
|
||||||
|
{
|
||||||
|
const int silence_samples = codec->output_sample_rate() / 5; // 200ms
|
||||||
|
std::vector<int16_t> silence(silence_samples, 0);
|
||||||
|
codec->OutputData(silence);
|
||||||
|
ESP_LOGI(TAG, "🔇 已灌 200ms 静音 PCM 覆盖 DMA 残留");
|
||||||
|
}
|
||||||
|
|
||||||
if (!player_pipeline_) {
|
if (!player_pipeline_) {
|
||||||
player_pipeline_ = player_pipeline_open();
|
player_pipeline_ = player_pipeline_open();
|
||||||
player_pipeline_run(player_pipeline_);
|
player_pipeline_run(player_pipeline_);
|
||||||
@ -2354,6 +2366,14 @@ void Application::OnAudioOutput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Application::OnAudioInput() {
|
void Application::OnAudioInput() {
|
||||||
|
// 🆕 移植自 Kapi commit b1577d8: hibernate 期间禁用输入侧, 允许 OnAudioOutput 跑(消费待命音队列)
|
||||||
|
// 防止 EnterIdleHibernate / WakeFromHibernate 过渡期间访问到关闭的 codec → std::bad_alloc abort
|
||||||
|
auto codec_for_guard = Board::GetInstance().GetAudioCodec();
|
||||||
|
if (hibernating_.load() || !codec_for_guard || !codec_for_guard->input_enabled() || !recorder_pipeline_) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(20));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<int16_t> data;
|
std::vector<int16_t> data;
|
||||||
|
|
||||||
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
|
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
|
||||||
@ -4451,37 +4471,28 @@ void Application::EnterIdleHibernate() {
|
|||||||
ESP_LOGW(TAG, "已处于休眠状态,跳过重复进入");
|
ESP_LOGW(TAG, "已处于休眠状态,跳过重复进入");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "🌙 进入空闲休眠:真退房 → 字幕提示(不熄屏)");
|
ESP_LOGI(TAG, "🌙 进入空闲休眠(方案C+ 移植自 Kapi b1577d8):stop RTC(保留 handle)→ 播待命音 → 字幕提示");
|
||||||
|
|
||||||
auto display = Board::GetInstance().GetDisplay();
|
auto display = Board::GetInstance().GetDisplay();
|
||||||
|
|
||||||
// [废弃方案] 静音填充曾尝试在此处用 codec->OutputData 填 200ms 静音覆盖 DMA 残留
|
|
||||||
// 但实测会让 ES7210 codec 进入卡死状态(连续 10 次重启 ES7210 I2C Open fail)
|
|
||||||
// 移除该方案,杂音问题需要用其他方式解决(如降低唤醒后初始音量)
|
|
||||||
|
|
||||||
// 1. 真退出 RTC 房间(释放 License)
|
|
||||||
// Protocol 基类的虚函数 LeaveRoom 默认回退到 CloseAudioChannel,
|
|
||||||
// VolcRtcProtocol 覆写为 volc_rtc_stop + volc_rtc_destroy
|
|
||||||
// 注意:LeaveRoom 内部会触发 on_audio_channel_closed_ 回调 → codec EnableOutput(false)
|
|
||||||
if (protocol_) {
|
|
||||||
protocol_->LeaveRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto codec = Board::GetInstance().GetAudioCodec();
|
auto codec = Board::GetInstance().GetAudioCodec();
|
||||||
|
|
||||||
// 3. 字幕显示推迟到最后做(此时 LVGL 锁竞争最少)— 见步骤 9
|
// 0. 🔴 最先置 hibernating_=true,阻止 AudioLoop 的 OnAudioInput 继续访问 codec
|
||||||
|
// OnAudioInput 入口有 guard 检测 hibernating_,但 OnAudioOutput 继续工作(消费待命音队列)
|
||||||
|
hibernating_.store(true);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50)); // 等 OnAudioInput 看到标志并跳过当前迭代
|
||||||
|
|
||||||
// 4. 显式关闭 codec input/output 让状态机重置(回调可能已关 output,这里幂等 + 关 input)
|
// 1. 退出 RTC 房间(仅 stop,保留 rtc_handle_;notify_closed=false 避免回调关 codec)
|
||||||
// 修复 bug:若不关闭,唤醒后 EnableInput(true) 会进入 "已 open" 异常路径
|
// LeaveRoom 内部同时重置 downlink_is_pcm_=false(关键!否则 PlaySound 的 Opus 会被当 PCM 写出 → 杂音)
|
||||||
// → esp_codec_dev_set_in_channel_gain ES_ERROR_CHECK 失败 abort
|
if (protocol_) {
|
||||||
if (codec) {
|
protocol_->LeaveRoom(/*notify_closed=*/false);
|
||||||
ESP_LOGI(TAG, "EnterIdleHibernate: 关闭 codec input/output 重置状态机");
|
|
||||||
codec->EnableInput(false);
|
|
||||||
codec->EnableOutput(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.5. 清空音频解码队列:阻止 hibernate 之前残留的 standby_sound / AI 半句 PCM
|
// 2. 等待 background_task 完成所有待解码任务,避免与本流程竞争 codec
|
||||||
// 在唤醒后的 OnAudioOutput 中被错误"首帧"识别,从而把软静音过早解开。
|
if (background_task_) {
|
||||||
|
background_task_->WaitForCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 清空音频解码队列:阻止 hibernate 之前残留的半句 PCM 数据干扰待命音
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
if (!audio_decode_queue_.empty()) {
|
if (!audio_decode_queue_.empty()) {
|
||||||
@ -4491,19 +4502,18 @@ void Application::EnterIdleHibernate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 关闭录音管道(避免唤醒后重新打开时冲突)
|
// 4. 关闭麦克风(codec output 保留,后面要播待命音)
|
||||||
|
// recorder_pipeline 也关掉避免下一帧 OnAudioInput 误读
|
||||||
|
if (codec) {
|
||||||
|
ESP_LOGI(TAG, "EnterIdleHibernate: 关闭 codec 麦克风(output 保留播待命音)");
|
||||||
|
codec->EnableInput(false);
|
||||||
|
}
|
||||||
if (recorder_pipeline_) {
|
if (recorder_pipeline_) {
|
||||||
recorder_pipeline_close(recorder_pipeline_);
|
recorder_pipeline_close(recorder_pipeline_);
|
||||||
recorder_pipeline_ = nullptr;
|
recorder_pipeline_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 关键时序:先设置 hibernating_=true(阻止 PowerSaveTimer 进入 Light Sleep)
|
// 5. 强制 esp_pm 禁用 Light Sleep(保护 I2C/codec 总线不睡)
|
||||||
// 再 SetDeviceState(kDeviceStateIdle)(之后 CanEnterSleepMode 会因为 hibernating_=true 而返回 false)
|
|
||||||
hibernating_.store(true);
|
|
||||||
|
|
||||||
// 6. 双保险:强制 esp_pm 禁用 Light Sleep(保护 I2C/codec 总线不进入睡眠)
|
|
||||||
// 防止 PowerSaveTimer 已经触发或紧接着触发 esp_pm_configure(light_sleep=true)
|
|
||||||
// 导致 ES7210/ES8311 唤醒后 I2C 通信失败 → ESP_ERROR_CHECK abort 重启
|
|
||||||
esp_pm_config_t pm_config = {
|
esp_pm_config_t pm_config = {
|
||||||
.max_freq_mhz = 240,
|
.max_freq_mhz = 240,
|
||||||
.min_freq_mhz = 240,
|
.min_freq_mhz = 240,
|
||||||
@ -4512,26 +4522,67 @@ 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 总线)");
|
||||||
|
|
||||||
// 7. 设备状态切回 idle(屏幕保持亮起,仅字幕提示)
|
// 5.5 🆕 强制重启 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 残留。
|
||||||
|
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)
|
||||||
SetDeviceState(kDeviceStateIdle);
|
SetDeviceState(kDeviceStateIdle);
|
||||||
|
|
||||||
// 8. 累计休眠次数(NVS 持久化)
|
// 7. 等待待命音播放完成(codec output 仍 enabled,AudioLoop OnAudioOutput 正常出队)
|
||||||
|
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_);
|
||||||
|
player_pipeline_ = nullptr;
|
||||||
|
ESP_LOGI(TAG, "EnterIdleHibernate: player_pipeline 已关闭");
|
||||||
|
} else if (codec) {
|
||||||
|
codec->EnableOutput(false);
|
||||||
|
ESP_LOGI(TAG, "EnterIdleHibernate: codec output 已关闭(无 pipeline 兜底)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 累计休眠次数(NVS 持久化)
|
||||||
idle_cycles_++;
|
idle_cycles_++;
|
||||||
SaveIdleCyclesToNvs();
|
SaveIdleCyclesToNvs();
|
||||||
ESP_LOGI(TAG, "✓ 已进入空闲休眠(累计第 %d 次)", idle_cycles_);
|
ESP_LOGI(TAG, "✓ 已进入空闲休眠(累计第 %d 次,rtc_handle 保留)", idle_cycles_);
|
||||||
if (idle_cycles_ >= IDLE_CYCLES_REBOOT_THRESHOLD) {
|
if (idle_cycles_ >= IDLE_CYCLES_REBOOT_THRESHOLD) {
|
||||||
ESP_LOGW(TAG, "🛡 累计休眠 %d 次(阈值 %d),下次唤醒触发硬重启清理内存碎片",
|
ESP_LOGW(TAG, "🛡 累计休眠 %d 次(阈值 %d),下次唤醒触发硬重启清理内存碎片",
|
||||||
idle_cycles_, IDLE_CYCLES_REBOOT_THRESHOLD);
|
idle_cycles_, IDLE_CYCLES_REBOOT_THRESHOLD);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 显示退出提示字幕(最后做:此时 LeaveRoom/SetDeviceState 都完成,
|
// 10. 显示退出提示字幕(数字人项目特有,Kapi 无屏底座没有此步骤)
|
||||||
// LVGL 锁竞争最少;带重试确保 LVGL 锁超时也能最终显示)
|
// 带重试确保 LVGL 锁超时也能最终显示
|
||||||
//
|
|
||||||
// 注意:ai_chat_set_chat_message 内部有 last_content 去重缓存,
|
|
||||||
// 锁超时时不更新缓存,下次重试相同内容时仍会进入 lvgl_port_lock 路径
|
|
||||||
const char* hibernate_msg = "已自动退出RTC对话,按BOOT键重新连接RTC";
|
const char* hibernate_msg = "已自动退出RTC对话,按BOOT键重新连接RTC";
|
||||||
for (int attempt = 0; attempt < 5; attempt++) {
|
for (int attempt = 0; attempt < 5; attempt++) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(200)); // 让出 CPU 给 LVGL 任务完成当前帧渲染
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
if (display) {
|
if (display) {
|
||||||
display->SetChatMessage("system", hibernate_msg);
|
display->SetChatMessage("system", hibernate_msg);
|
||||||
}
|
}
|
||||||
@ -4566,16 +4617,24 @@ void Application::WakeFromHibernate() {
|
|||||||
display->SetChatMessage("system", "");
|
display->SetChatMessage("system", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 先复位为 idle 状态(EnterIdleHibernate 已设为 idle,这里幂等)
|
// 3. 🆕 移植自 Kapi commit b1577d8: 先放下 hibernating_,让 AudioLoop 的
|
||||||
|
// OnAudioInput guard 通过, 再 ToggleChatState。否则 ToggleChatState 期间
|
||||||
|
// OnAudioInput 仍会被 hibernating_=true 拦截,导致音频上行迟迟不开。
|
||||||
|
hibernating_.store(false);
|
||||||
|
|
||||||
|
// 4. 复位为 idle(EnterIdleHibernate 已设为 idle,此处幂等)
|
||||||
if (device_state_ != kDeviceStateIdle) {
|
if (device_state_ != kDeviceStateIdle) {
|
||||||
SetDeviceState(kDeviceStateIdle);
|
SetDeviceState(kDeviceStateIdle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 触发 RTC 重连(复用 ToggleChatState → OpenAudioChannel → 自动重建 rtc_handle_)
|
// 5. 触发 RTC 重连
|
||||||
ESP_LOGI(TAG, "WakeFromHibernate: 调用 ToggleChatState() 触发 RTC 重连...");
|
// - LeaveRoom 只 stop 没 destroy,rtc_handle_ 仍然有效
|
||||||
|
// - OpenAudioChannel 检查 rtc_handle_ 通过 → volc_rtc_start → 成功
|
||||||
|
// - on_audio_channel_opened_ 回调里会 EnableOutput(true) + 200ms silence + 开 player_pipeline
|
||||||
|
// - 进入 kDeviceStateDialog 后 recorder_pipeline 也会重开 + EnableInput(true)
|
||||||
|
ESP_LOGI(TAG, "WakeFromHibernate: 调用 ToggleChatState() 触发 RTC 重连(rtc_handle 复用)...");
|
||||||
ToggleChatState();
|
ToggleChatState();
|
||||||
|
|
||||||
hibernating_.store(false);
|
ESP_LOGI(TAG, "✓ 唤醒完成,已触发 RTC 重连");
|
||||||
ESP_LOGI(TAG, "✓ 唤醒完成,已触发 RTC 重连(注意:实际重连进度由 ToggleChatState 异步处理)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,8 +55,10 @@ public:
|
|||||||
virtual bool OpenAudioChannel() = 0;
|
virtual bool OpenAudioChannel() = 0;
|
||||||
virtual void CloseAudioChannel() = 0;
|
virtual void CloseAudioChannel() = 0;
|
||||||
// Phase 6: 真退出 RTC 房间(释放 License),默认回退到 CloseAudioChannel
|
// Phase 6: 真退出 RTC 房间(释放 License),默认回退到 CloseAudioChannel
|
||||||
// VolcRtcProtocol 覆写:调用 volc_rtc_stop + volc_rtc_destroy
|
// notify_closed=true: 触发 on_audio_channel_closed_ 回调(默认,兼容老路径)
|
||||||
virtual void LeaveRoom() { CloseAudioChannel(); }
|
// notify_closed=false: 不触发回调,供 EnterIdleHibernate 使用——避免回调里的
|
||||||
|
// player_pipeline_close → EnableOutput(false) 误关 codec output 导致待命音无声
|
||||||
|
virtual void LeaveRoom(bool notify_closed = true) { (void)notify_closed; CloseAudioChannel(); }
|
||||||
virtual bool IsAudioChannelOpened() const = 0;
|
virtual bool IsAudioChannelOpened() const = 0;
|
||||||
virtual void SendAudio(const std::vector<uint8_t>& data) = 0;
|
virtual void SendAudio(const std::vector<uint8_t>& data) = 0;
|
||||||
virtual void SendPcm(const std::vector<uint8_t>& data) {}
|
virtual void SendPcm(const std::vector<uint8_t>& data) {}
|
||||||
|
|||||||
@ -423,22 +423,31 @@ void VolcRtcProtocol::CloseAudioChannel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 6: 真退出 RTC 房间(释放 License)
|
// Phase 6+ 移植自 Kapi commit b1577d8: 退出 RTC 房间
|
||||||
// = volc_rtc_stop + volc_rtc_destroy
|
// 仅 volc_rtc_stop,保留 rtc_handle_,供唤醒后 OpenAudioChannel 复用(再次 volc_rtc_start)
|
||||||
// 火山官方文档(StopVoiceChat 说明): 真人退房必须 leaveRoom + destroyRTCEngine
|
// 如果走 destroy 路径,唤醒时 rtc_handle_=nullptr → OpenAudioChannel 直接失败 → 死循环
|
||||||
// 客户端调用 volc_rtc_destroy 后,服务端 AI 任务会在 180s 内自动停止
|
// 服务端 AI 任务在客户端 stop 后无需 destroy 也会按 180s 兜底机制清理
|
||||||
void VolcRtcProtocol::LeaveRoom() {
|
// notify_closed=false 时跳过 on_audio_channel_closed_,避免回调里 player_pipeline_close
|
||||||
|
// → EnableOutput(false) 把 codec output 关掉导致 hibernate 期间待命音无声
|
||||||
|
void VolcRtcProtocol::LeaveRoom(bool notify_closed) {
|
||||||
if (rtc_handle_) {
|
if (rtc_handle_) {
|
||||||
if (is_connected_) {
|
if (is_connected_) {
|
||||||
volc_rtc_stop(rtc_handle_);
|
volc_rtc_stop(rtc_handle_);
|
||||||
is_connected_ = false;
|
is_connected_ = false;
|
||||||
}
|
}
|
||||||
volc_rtc_destroy(rtc_handle_);
|
ESP_LOGI(TAG, "✓ 已 stop RTC 房间(保留 handle 供唤醒复用, notify_closed=%d)", (int)notify_closed);
|
||||||
rtc_handle_ = nullptr;
|
|
||||||
ESP_LOGI(TAG, "✓ 已真退出 RTC 房间(leaveRoom + destroyRTCEngine)");
|
|
||||||
}
|
}
|
||||||
is_audio_channel_opened_ = false;
|
is_audio_channel_opened_ = false;
|
||||||
if (on_audio_channel_closed_) {
|
|
||||||
|
// 🔴 关键修复:重置下行音频格式标志位
|
||||||
|
// 原因: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_();
|
on_audio_channel_closed_();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,10 @@ public:
|
|||||||
bool OpenAudioChannel() override;// 🔊 打开音频通道
|
bool OpenAudioChannel() override;// 🔊 打开音频通道
|
||||||
void CloseAudioChannel() override;// 🔊 关闭音频通道(仅 stop 媒体流,不退出房间)
|
void CloseAudioChannel() override;// 🔊 关闭音频通道(仅 stop 媒体流,不退出房间)
|
||||||
|
|
||||||
// Phase 6: 真退出 RTC 房间 = volc_rtc_stop + volc_rtc_destroy(释放 License)
|
// Phase 6+:退出 RTC 房间(仅 stop,保留 rtc_handle_ 供唤醒复用)
|
||||||
|
// notify_closed=false 时跳过 on_audio_channel_closed_,hibernate 路径用,避免 codec 被回调链关
|
||||||
// 与 CloseAudioChannel 区别:CloseAudioChannel 只停媒体流,房间仍占用
|
// 与 CloseAudioChannel 区别:CloseAudioChannel 只停媒体流,房间仍占用
|
||||||
void LeaveRoom() override;
|
void LeaveRoom(bool notify_closed = true) override;
|
||||||
|
|
||||||
bool IsAudioChannelOpened() const override;// 🔊 检查音频通道是否已打开
|
bool IsAudioChannelOpened() const override;// 🔊 检查音频通道是否已打开
|
||||||
void SendAbortSpeaking(AbortReason reason) override;// 🔊 发送中止通话请求
|
void SendAbortSpeaking(AbortReason reason) override;// 🔊 发送中止通话请求
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user