Baji_Rtc_Toy/main/application.h
Rdzleo c6ecdb124c feat(audio): 同步 Kapi 软件 AEC + NULL crash fix 到数字人项目
来源: Kapi commit 63b21fd (NULL fix) + a369796 (软件 AEC) 完整移植

改动:
- components/common/src/volc_rtc.c
  on_global_error 加 message ? message : "(null)" 防御.
  解决: idle ≥10 分钟后服务端 session 超时 / NAT 表过期触发的
       printf("%s", NULL) → strlen panic → 设备重启.

- main/CMakeLists.txt
  REQUIRES 末尾加 esp-sr (提供 esp_aec.h 同步 API).

- main/application.h
  +7 个 AEC 成员 + 5 个函数声明 (recorder/player pipeline 后).

- main/application.cc
  +InitAec / DeinitAec / AppendRefSamples / GetDelayedRef / ApplyAEC (~170 行).
  OnAudioOutput 三个 codec output 位置都加 AppendRefSamples hook.
  ReadAudio 两条路径 (recorder_pipeline + codec 直读) 加 lazy InitAec
  + ApplyAEC + target_samples = max(samples, chunk_size).
  PHASE8_DIAG_ENABLE 默认 0 (诊断埋点已完成根因定位, 关闭减少日志噪声).

实测效果 (30 分钟设备验证):
  AI 说话:    mic=285 ref=8310 clean=31 (消除 89%)
  AI 说话:    mic=660 ref=7489 clean=57 (消除 91%)
  AI 说话:    mic=156 ref=2748 clean=0  (消除 100%)
  用户说话:   mic=224 ref=8 clean=224   (passthrough 正常)

资源占用:
  Flash       +59 KB (+1.2%)
  Internal SRAM  +35-50 KB (+10-15%)
  PSRAM       +10-15 KB (<0.2%)
  Core 1 CPU  +6-12% @240MHz

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:54:55 +08:00

245 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#ifndef _APPLICATION_H_
#define _APPLICATION_H_
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <freertos/task.h>
#include <esp_timer.h>
#include <string>
#include <mutex>
#include <list>
#include <atomic>
#include <opus_encoder.h>
#include <opus_decoder.h>
#include <opus_resampler.h>
#include "protocol.h"
#include "websocket_protocol.h"
#include "ota.h"
#include "background_task.h"
#include "audio/simple_pipeline.h"
// #include "ble_service.h" // BLE JSON Service 暂不使用
#if CONFIG_USE_WAKE_WORD_DETECT
#include "wake_word_detect.h"
#elif CONFIG_USE_CUSTOM_WAKE_WORD
#include "custom_wake_word.h"
#endif
#if CONFIG_USE_AUDIO_PROCESSOR
#include "audio_processor.h"
#endif
#define SCHEDULE_EVENT (1 << 0)
#define AUDIO_INPUT_READY_EVENT (1 << 1)
#define AUDIO_OUTPUT_READY_EVENT (1 << 2)
// 未知状态、启动中、WiFi配网模式、空闲待命、连接服务器、语音监听中、语音播报中、固件升级中、设备激活中、致命错误
enum DeviceState {
kDeviceStateUnknown,
kDeviceStateStarting,
kDeviceStateWifiConfiguring,
kDeviceStateIdle,
kDeviceStateConnecting,
kDeviceStateListening,
kDeviceStateSpeaking,
kDeviceStateDialog,
kDeviceStateUpgrading,
kDeviceStateActivating,
kDeviceStateFatalError
};
// OPUS音频帧时长60ms
#define OPUS_FRAME_DURATION_MS 60
// 应用程序主类(单例模式)
class Application {
public:
static Application& GetInstance() {
static Application instance;
return instance;
}
// 删除拷贝构造函数和赋值运算符
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
void Start(); // 启动应用程序
DeviceState GetDeviceState() const { return device_state_; } // 获取当前状态
bool IsVoiceDetected() const { return voice_detected_; } // 语音检测状态
void Schedule(std::function<void()> callback); // 任务调度
void SetDeviceState(DeviceState state); // 状态变更
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");// 警报管理 状态、消息、情感、声音
void DismissAlert();// 关闭警报
void AbortSpeaking(AbortReason reason);// 打断语音播报
void AbortHttpsPlayback(const char* reason);// 中止HTTPS音频播放并清空DMA
void HttpsPlaybackFromUrl(const std::string& url); // 通过HTTPS下载JSON并播放音频故事/歌曲等)
void SendStoryRequest(); // 通过HTTPS故事API请求并播放故事
void SendMusicRequest(); // 通过HTTPS音乐API请求并播放音乐
void ToggleChatState();// 切换聊天状态
void ToggleListeningState();// 切换监听状态
void StartListening();// 开始监听
void StopListening();// 停止监听
void SendTextMessage(const std::string& text);// 发送文本消息
void UpdateIotStates();// 更新IOT设备状态
void Reboot();// 系统重启
void WakeWordInvoke(const std::string& wake_word);// 唤醒词回调
void PlaySound(const std::string_view& sound);// 播放声音
void WaitForAudioPlayback();// 等待音频播报完成
bool IsAudioQueueEmpty(); // 检查音频队列是否为空
void ClearAudioQueue(); // 清空音频播放队列
bool CanEnterSleepMode();// 检查是否可以进入睡眠模式
void StopAudioProcessor();// 停止音频处理器
void ResetDecoder();// 重置解码器状态(用于修复音频播放问题)
bool IsSafeToOperate(); // 🔧 检查当前是否可以安全执行操作
void AbortSpeakingAndReturnToIdle(); // 🔴 专门处理从说话状态到空闲状态的切换
void AbortSpeakingAndReturnToListening(); // 🔵 专门处理从说话状态到聆听状态的切换
void PauseAudioPlayback(); // ⏸️ 暂停音频播放
void ResumeAudioPlayback(); // ▶️ 恢复音频播放
void SuppressNextIdleSound(); // 🔇 抑制下一个空闲状态的声音播放
void SetLowBatteryTransition(bool value);
bool IsLowBatteryTransition() const;
void InitializeWebsocketProtocol(); // 🌐 初始化WebSocket协议RTC连接成功后调用
// void SendTextViaWebsocket(const std::string& text);// 🌐 通过WebSocket发送文本消息
// 姿态传感器接口
bool IsImuSensorAvailable(); // 检查IMU传感器是否可用
bool GetImuData(float* acc_x, float* acc_y, float* acc_z,
float* gyro_x, float* gyro_y, float* gyro_z,
float* temperature); // 获取IMU传感器数据
void OnMotionDetected(); // 运动检测事件处理
bool IsAudioPaused() const { return audio_paused_; } // 检查音频是否暂停
bool ShouldSkipDialogIdleSession() const { return skip_dialog_idle_session_; }// 是否跳过对话待机会话
void ClearDialogIdleSkipSession();// 清除对话待机会话标志位
bool IsDialogUploadEnabled() const { return dialog_upload_enabled_; }// 是否启用对话上传
void SetDialogUploadEnabled(bool enabled);// 设置对话上传状态
// Phase 6: 空闲休眠相关
void EnterIdleHibernate(); // 进入空闲休眠(字幕+真退房+熄屏)
void WakeFromHibernate(); // 从休眠唤醒(亮屏+重连)
bool IsHibernating() const { return hibernating_.load(); }
// // BLE JSON 命令处理(暂不使用)
// void HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data, BleJsonService& service);
private:
Application();// 构造函数
~Application();// 析构函数
// 配置使用唤醒词检测
#if CONFIG_USE_WAKE_WORD_DETECT
WakeWordDetect wake_word_detect_;
#elif CONFIG_USE_CUSTOM_WAKE_WORD
CustomWakeWord wake_word_detect_;
#endif
// 音频处理器
#if CONFIG_USE_AUDIO_PROCESSOR
AudioProcessor audio_processor_;
#endif
Ota ota_;
std::mutex mutex_;
std::list<std::function<void()>> main_tasks_;
std::unique_ptr<Protocol> protocol_;
std::unique_ptr<WebsocketProtocol> websocket_protocol_; // 🌐 WebSocket协议实例RTC连接后初始化
EventGroupHandle_t event_group_ = nullptr;
esp_timer_handle_t clock_timer_handle_ = nullptr;
volatile DeviceState device_state_ = kDeviceStateUnknown;
std::atomic<bool> is_aborting_{false}; // 🔧 原子标志:防止重复中止操作
std::atomic<std::chrono::steady_clock::time_point> last_safe_operation_; // 🔧 最后安全操作时间戳
std::atomic<bool> is_switching_to_listening_{false}; // 🔵 标志:正在主动切换到聆听状态
std::atomic<bool> is_low_battery_transition_{false};
// Phase 6: 空闲休眠状态
std::atomic<bool> hibernating_{false}; // 是否处于熄屏休眠状态
int idle_cycles_ = 0; // 累计休眠循环次数NVS 持久化)
static constexpr int IDLE_CYCLES_REBOOT_THRESHOLD = 50; // 累计 50 次触发硬重启清碎片
void SaveIdleCyclesToNvs();
void LoadIdleCyclesFromNvs();
void ResetIdleCyclesNvs();
ListeningMode listening_mode_ = kListeningModeAutoStop;
#if CONFIG_USE_REALTIME_CHAT
bool realtime_chat_enabled_ = true;
#else
bool realtime_chat_enabled_ = false;
#endif
std::atomic<bool> ws_downlink_enabled_{true};// 🌐 WebSocket下行通道是否启用
std::atomic<bool> ws_playback_active_{false};// 🌐 WebSocket下行播放活跃标志
std::atomic<bool> opus_playback_active_{false};// Opus解码播放活跃标志WS/HTTPS共用
std::atomic<bool> https_playback_active_{false};// HTTPS音频播放进行中标志
std::atomic<bool> https_playback_abort_{false};// HTTPS音频播放中止标志
std::atomic<int> post_abort_debug_frames_{0};// HTTPS中止后诊断日志计数追踪前N帧音频
bool aborted_ = false;
bool voice_detected_ = false;
bool audio_paused_ = false; // 音频暂停状态标志
float current_speaker_volume_ = 0.0f; // 当前扬声器音量,用于语音打断判断
bool provisioning_mode_ = false; // 配网模式标志缓存避免重复读NVS
bool first_idle_location_checked_ = false;// 是否首次查询城市天气
bool send_pcm_uplink_ = true; // 是否发送PCM音频数据到服务器由SDK内部转码为G711A
bool send_g711a_uplink_ = false;// 是否直接发送G711A音频数据到服务器
std::chrono::time_point<std::chrono::steady_clock> last_audio_input_time_;
std::chrono::time_point<std::chrono::steady_clock> last_audible_output_time_; // 最后一次有声音输出的时间点
bool skip_dialog_idle_session_; // 是否跳过对话待机会话标志
bool dialog_upload_enabled_ = true; // 对话上传状态标志
bool dialog_watchdog_running_; // 对话看门狗运行标志
int dialog_watchdog_last_logged_; // 对话看门狗上次记录的日志时间
TaskHandle_t dialog_watchdog_task_handle_; // 对话看门狗任务句柄
int clock_ticks_;
TaskHandle_t main_loop_task_handle_;
TaskHandle_t check_new_version_task_handle_;
// Audio encode / decode
TaskHandle_t audio_loop_task_handle_;
BackgroundTask* background_task_;
std::chrono::steady_clock::time_point last_output_time_;
std::list<std::vector<uint8_t>> audio_decode_queue_;
std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
OpusResampler input_resampler_;// 输入音频采样器
OpusResampler reference_resampler_;// 参考音频采样器
OpusResampler output_resampler_;// 输出音频采样器
OpusResampler uplink_resampler_;// 上传音频采样器
player_pipeline_handle_t player_pipeline_ = nullptr;
recorder_pipeline_handle_t recorder_pipeline_ = nullptr;
// 路径 D'' AEC: esp_aec.h 底层同步 API + 软件 loopback ref
// codec 保持 baseline 1ch 16-bit (MIC1|MIC2 ES7210 内部混合 mono)
// DAC 输出 PCM 同步复制到 ref_ring_buf, ReadAudio 调 aec_process(mic, delayed_ref) → clean
// ⚠️ portMUX (spinlock) 会禁用本核中断, 与 WiFi 协议栈 pm_coex_set_reconnect_policy 冲突
// 实测引发 IllegalInstruction panic。改用 FreeRTOS mutex (不禁中断, 仅 task 间互斥)
void *aec_handle_ = nullptr; // aec_handle_t* (避免暴露 esp_aec.h 类型)
int aec_chunk_size_ = 0; // aec_get_chunksize 返回 (16k 通常 256 samples = 16ms)
int16_t *ref_ring_buf_ = nullptr; // PSRAM 上分配 ~200ms ref ring buffer
int ref_ring_capacity_ = 0;
int ref_ring_write_idx_ = 0;
int ref_ring_filled_ = 0;
int aec_ref_delay_samples_ = 800; // 延迟补偿 samples (默认 50ms @16kHz, 调优范围 30-80ms)
SemaphoreHandle_t ref_ring_mutex_ = nullptr;
void InitAec();
void DeinitAec();
void AppendRefSamples(const int16_t *pcm, int samples); // OnAudioOutput 调用, DAC PCM 推入 ring buffer
void GetDelayedRef(int16_t *ref_out, int samples); // ApplyAEC 内部使用, 取延迟后 ref
void ApplyAEC(std::vector<int16_t>& mic_inout); // ReadAudio 调用, in-place 处理 mic → clean
void MainLoop();// 主事件循环
void OnAudioInput();// 音频输入回调
void OnAudioOutput();// 音频输出回调
void ReadAudio(std::vector<int16_t>& data, int sample_rate, int samples);// 读取音频数据
void SetDecodeSampleRate(int sample_rate, int frame_duration);// 设置解码采样率
void CheckNewVersion();// 检查新固件版本
void ShowActivationCode();// 显示激活码
void OnClockTimer();// 时钟定时器回调
void SetListeningMode(ListeningMode mode);// 设置监听模式
void AudioLoop();// 音频处理循环
bool suppress_next_idle_sound_ = false;// 标志:是否抑制下一个空闲状态的声音播放
void StartDialogWatchdog();// 启动对话看门狗
void StopDialogWatchdog(); // 停止对话看门狗
void HttpsApiPlayback(const char* api_url_base, const char* tag, const char* task_name); // HTTPS API音频播放通用实现
const char* DeviceStateToString(DeviceState state); // 状态枚举转字符串
};
#endif // _APPLICATION_H_