一、利用創建好的對象來調用音頻服務?
上周從上圖的getaudiocode()方法進去感受了一下底層小智的構造如何實現。所以用一個codec來接收我們所構造的音頻對象。
下來是用構造好的音頻對象來調用音頻初始化服務Initialize,因為啟動函數Application函數的類中有audio_servicez_所以可以進行調用。
這段初始化代碼的核心作用是:
1綁定并啟動音頻編解碼器
2配置音頻數據流的格式和處理流程
3按需初始化音頻處理器和喚醒詞檢測模塊
4設置好各類回調,保證音頻事件能及時通知到主程序
5創建定時器,自動管理音頻硬件電源
void AudioService::Initialize(AudioCodec* codec) {// 保存傳入的音頻編解碼器指針codec_ = codec;// 啟動音頻編解碼器,準備采集和播放codec_->Start();/* 初始化 Opus 解碼器和編碼器 */// 創建 Opus 解碼器,采樣率與輸出一致,單聲道,幀長為 OPUS_FRAME_DURATION_MSopus_decoder_ = std::make_unique<OpusDecoderWrapper>(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS);// 創建 Opus 編碼器,采樣率固定為 16kHz,單聲道,幀長為 OPUS_FRAME_DURATION_MSopus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);// 設置編碼復雜度為最低,節省算力opus_encoder_->SetComplexity(0);// 如果輸入采樣率不是 16kHz,則配置重采樣器,將輸入音頻轉換為 16kHzif (codec->input_sample_rate() != 16000) {input_resampler_.Configure(codec->input_sample_rate(), 16000);reference_resampler_.Configure(codec->input_sample_rate(), 16000);}// 根據編譯配置選擇不同的音頻處理器(如帶有回聲消除的AFE,或無處理的空實現)
#if CONFIG_USE_AUDIO_PROCESSORaudio_processor_ = std::make_unique<AfeAudioProcessor>();
#elseaudio_processor_ = std::make_unique<NoAudioProcessor>();
#endif// 根據編譯配置選擇不同的喚醒詞檢測算法
#if CONFIG_USE_AFE_WAKE_WORDwake_word_ = std::make_unique<AfeWakeWord>();
#elif CONFIG_USE_ESP_WAKE_WORDwake_word_ = std::make_unique<EspWakeWord>();
#elif CONFIG_USE_CUSTOM_WAKE_WORDwake_word_ = std::make_unique<CustomWakeWord>();
#elsewake_word_ = nullptr;
#endif// 設置音頻處理器的輸出回調,當有處理好的音頻輸出時,推入編碼隊列audio_processor_->OnOutput([this](std::vector<int16_t>&& data) {PushTaskToEncodeQueue(kAudioTaskTypeEncodeToSendQueue, std::move(data));});// 設置語音活動檢測(VAD)回調,檢測到說話狀態變化時,更新狀態并通知外部audio_processor_->OnVadStateChange([this](bool speaking) {voice_detected_ = speaking;if (callbacks_.on_vad_change) {callbacks_.on_vad_change(speaking);}});// 如果啟用了喚醒詞檢測,設置喚醒詞檢測回調,檢測到喚醒詞時通知外部if (wake_word_) {wake_word_->OnWakeWordDetected([this](const std::string& wake_word) {if (callbacks_.on_wake_word_detected) {callbacks_.on_wake_word_detected(wake_word);}});}// 創建音頻電源管理定時器,定期檢查音頻輸入/輸出是否需要關閉以省電esp_timer_create_args_t audio_power_timer_args = {.callback = [](void* arg) {AudioService* audio_service = (AudioService*)arg;audio_service->CheckAndUpdateAudioPowerState();},.arg = this,.dispatch_method = ESP_TIMER_TASK,.name = "audio_power_timer",.skip_unhandled_events = true,};esp_timer_create(&audio_power_timer_args, &audio_power_timer_);
}
二、啟動音頻服務
經過上部分的初始化,配置好了音頻的編解碼器,以及處理時對于音頻的要求(不符合要求的要重新采樣為符合要求的格式),還包括喚醒詞的檢測、提取和回調。
啟動流程(Start)
1標記服務未停止
service_stopped_ = false;
讓各任務知道服務正在運行。
2清除音頻相關事件位
xEventGroupClearBits(...)
確保音頻輸入、喚醒詞、音頻處理等任務可以正常啟動。
3啟動音頻電源管理定時器
esp_timer_start_periodic(...)
每秒檢查一次音頻硬件的電源狀態,自動省電。
4啟動音頻輸入任務
xTaskCreatePinnedToCore?或?xTaskCreate
創建音頻采集任務,負責從麥克風采集音頻數據。
5啟動音頻輸出任務
xTaskCreate
創建音頻播放任務,負責將音頻數據輸出到揚聲器。
6啟動 Opus?編解碼任務
xTaskCreate
創建音頻編解碼任務,負責音頻數據的編碼(發送)和解碼(播放)。
void AudioService::Start() {// 標記服務未停止service_stopped_ = false;// 清除音頻相關的事件位,確保任務可以正常啟動xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING | AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING);// 啟動音頻電源管理定時器,每秒檢查一次音頻硬件電源狀態esp_timer_start_periodic(audio_power_timer_, 1000000);/* 啟動音頻輸入任務 */
#if CONFIG_USE_AUDIO_PROCESSOR// 如果使用音頻處理器,任務綁定到指定內核xTaskCreatePinnedToCore([](void* arg) {AudioService* audio_service = (AudioService*)arg;audio_service->AudioInputTask();vTaskDelete(NULL);}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_, 1);
#else// 不使用音頻處理器,普通方式創建任務xTaskCreate([](void* arg) {AudioService* audio_service = (AudioService*)arg;audio_service->AudioInputTask();vTaskDelete(NULL);}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_);
#endif/* 啟動音頻輸出任務 */xTaskCreate([](void* arg) {AudioService* audio_service = (AudioService*)arg;audio_service->AudioOutputTask();vTaskDelete(NULL);}, "audio_output", 4096, this, 3, &audio_output_task_handle_);/* 啟動 Opus 編解碼任務 */xTaskCreate([](void* arg) {AudioService* audio_service = (AudioService*)arg;audio_service->OpusCodecTask();vTaskDelete(NULL);}, "opus_codec", 4096 * 7, this, 2, &opus_codec_task_handle_);
}
三、音頻回調服務
下來回到Application函數內,下一步執行下圖這一模塊:?
首先定義一個callbacks對象,他的類型如下:
?AudioServiceCallbacks?是一個回調函數集合,用于讓外部(比如主應用 Application)能夠“訂閱”音頻服務(AudioService)中的各種事件。當音頻服務內部發生特定事件時,會自動調用這些回調,通知外部進行相應處理。
struct AudioServiceCallbacks {std::function<void(void)> on_send_queue_available;std::function<void(const std::string&)> on_wake_word_detected;std::function<void(bool)> on_vad_change;std::function<void(void)> on_audio_testing_queue_full; };
讓主程序通過事件組機制,能夠及時響應音頻服務中的關鍵事件,實現音頻事件的異步通知和處理。?
?
每個成員的含義
- on_send_queue_available
類型:std::function<void(void)>
說明:當音頻發送隊列有可用數據時觸發。比如可以通知主程序“可以發送音頻數據到服務器了”。
- on_wake_word_detected
類型:std::function<void(const std::string&)>
說明:當檢測到喚醒詞(如“小智”)時觸發。參數是檢測到的喚醒詞內容。
- on_vad_change
類型:std::function<void(bool)>
說明:當語音活動檢測(VAD)狀態發生變化時觸發。參數?bool?表示當前是否有人在說話(true=正在說話,false=靜音)。
- on_audio_testing_queue_full
類型:std::function<void(void)>
說明:當音頻測試隊列已滿時觸發。一般用于調試或測試場景。
異步和函數回調的區別?
方面 | 異步執行 | 自動回調 |
---|---|---|
是否并發 | 是,任務后臺運行 | 不一定,回調是響應機制 |
主體是誰 | 程序發起的異步任務 | 異步任務完成后執行的函數 |
控制權 | 主程序不阻塞,控制權立即返回 | 控制權在回調被觸發時才回到你手里 |
是否依賴異步 | 異步通常搭配回調使用 | 回調常用在異步任務,但也可用于同步場景 |
舉個例子 | setTimeout() 不會阻塞主線程 | setTimeout(fn, 1000) 中的 fn 是回調 |
四、音頻服務具體功能
分別了解下列三個核心任務函數:
- AudioInputTask():音頻采集
- AudioOutputTask():音頻播放
- OpusCodecTask():音頻編解碼
// 音頻輸入任務,運行在一個 FreeRTOS 任務中
void AudioService::AudioInputTask() {while (true) {// 等待音頻相關事件觸發:測試模式、喚醒詞檢測、通用音頻處理EventBits_t bits = xEventGroupWaitBits(event_group_,AS_EVENT_AUDIO_TESTING_RUNNING |AS_EVENT_WAKE_WORD_RUNNING |AS_EVENT_AUDIO_PROCESSOR_RUNNING,pdFALSE, // 不清除標志位pdFALSE, // 任意一個事件即可返回portMAX_DELAY // 無限等待);// 如果服務已經停止,則退出任務if (service_stopped_) {break;}// 若麥克風需要預熱,延遲一段時間后繼續下一輪循環if (audio_input_need_warmup_) {audio_input_need_warmup_ = false;vTaskDelay(pdMS_TO_TICKS(120)); // 延遲 120mscontinue;}/** ==========================* 音頻測試處理邏輯(如按下 BOOT 錄音)* ========================== */if (bits & AS_EVENT_AUDIO_TESTING_RUNNING) {// 判斷測試隊列是否已滿(按最大時長判斷)if (audio_testing_queue_.size() >= AUDIO_TESTING_MAX_DURATION_MS / OPUS_FRAME_DURATION_MS) {ESP_LOGW(TAG, "Audio testing queue is full, stopping audio testing");EnableAudioTesting(false); // 自動關閉測試continue;}// 準備讀取一幀音頻數據(例如 20ms × 16000Hz)std::vector<int16_t> data;int samples = OPUS_FRAME_DURATION_MS * 16000 / 1000;// 如果成功讀取音頻數據if (ReadAudioData(data, 16000, samples)) {// 若為雙聲道,僅保留左聲道數據(變為單聲道)if (codec_->input_channels() == 2) {auto mono_data = std::vector<int16_t>(data.size() / 2);for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {mono_data[i] = data[j];}data = std::move(mono_data);}// 推送數據到測試編碼隊列PushTaskToEncodeQueue(kAudioTaskTypeEncodeToTestingQueue, std::move(data));continue; // 當前處理完畢,回到等待下一次事件}}/** ==========================* 喚醒詞檢測處理邏輯* ========================== */if (bits & AS_EVENT_WAKE_WORD_RUNNING) {std::vector<int16_t> data;int samples = wake_word_->GetFeedSize(); // 獲取所需幀長度// 若幀長度有效且成功讀取數據if (samples > 0 && ReadAudioData(data, 16000, samples)) {wake_word_->Feed(data); // 投喂喚醒詞檢測器continue;}}/** ==========================* 通用音頻處理邏輯* ========================== */if (bits & AS_EVENT_AUDIO_PROCESSOR_RUNNING) {std::vector<int16_t> data;int samples = audio_processor_->GetFeedSize(); // 獲取處理器需要的數據大小// 若幀有效且數據讀取成功if (samples > 0 && ReadAudioData(data, 16000, samples)) {audio_processor_->Feed(std::move(data)); // 投喂音頻處理器continue;}}// 如果沒有任何已知事件被處理到,這通常是邏輯錯誤ESP_LOGE(TAG, "Should not be here, bits: %lx", bits);break; // 退出任務}// 最后,任務退出時打印警告日志ESP_LOGW(TAG, "Audio input task stopped");
}
void AudioService::AudioInputTask() {while (true) {EventBits_t bits = xEventGroupWaitBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING |AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING,pdFALSE, pdFALSE, portMAX_DELAY);if (service_stopped_) {break;}if (audio_input_need_warmup_) {audio_input_need_warmup_ = false;vTaskDelay(pdMS_TO_TICKS(120));continue;}/* Used for audio testing in NetworkConfiguring mode by clicking the BOOT button */if (bits & AS_EVENT_AUDIO_TESTING_RUNNING) {if (audio_testing_queue_.size() >= AUDIO_TESTING_MAX_DURATION_MS / OPUS_FRAME_DURATION_MS) {ESP_LOGW(TAG, "Audio testing queue is full, stopping audio testing");EnableAudioTesting(false);continue;}std::vector<int16_t> data;int samples = OPUS_FRAME_DURATION_MS * 16000 / 1000;if (ReadAudioData(data, 16000, samples)) {// If input channels is 2, we need to fetch the left channel dataif (codec_->input_channels() == 2) {auto mono_data = std::vector<int16_t>(data.size() / 2);for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {mono_data[i] = data[j];}data = std::move(mono_data);}PushTaskToEncodeQueue(kAudioTaskTypeEncodeToTestingQueue, std::move(data));continue;}}/* Feed the wake word */if (bits & AS_EVENT_WAKE_WORD_RUNNING) {std::vector<int16_t> data;int samples = wake_word_->GetFeedSize();if (samples > 0) {if (ReadAudioData(data, 16000, samples)) {wake_word_->Feed(data);continue;}}}/* Feed the audio processor */if (bits & AS_EVENT_AUDIO_PROCESSOR_RUNNING) {std::vector<int16_t> data;int samples = audio_processor_->GetFeedSize();if (samples > 0) {if (ReadAudioData(data, 16000, samples)) {audio_processor_->Feed(std::move(data));continue;}}}ESP_LOGE(TAG, "Should not be here, bits: %lx", bits);break;}ESP_LOGW(TAG, "Audio input task stopped");
}
?
void AudioService::AudioOutputTask() {while (true) {// 加鎖等待播放隊列非空或服務停止信號std::unique_lock<std::mutex> lock(audio_queue_mutex_);// 如果隊列為空且服務未停止,則阻塞等待條件變量觸發audio_queue_cv_.wait(lock, [this]() { return !audio_playback_queue_.empty() || service_stopped_; });// 如果檢測到服務已經停止,則退出任務if (service_stopped_) {break;}// 從播放隊列取出一個音頻任務(前移出隊)auto task = std::move(audio_playback_queue_.front());audio_playback_queue_.pop_front();// 通知等待的線程隊列已發生變化(喚醒可能的生產者)audio_queue_cv_.notify_all();// 解鎖互斥量,開始進行播放處理lock.unlock();// 如果音頻輸出尚未啟用,則啟用輸出并啟動功耗監測定時器if (!codec_->output_enabled()) {codec_->EnableOutput(true);esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000);}// 將 PCM 數據輸出到音頻設備codec_->OutputData(task->pcm);// 更新時間戳記錄為最近一次輸出時間last_output_time_ = std::chrono::steady_clock::now();// 播放計數器 +1,用于調試/統計debug_statistics_.playback_count++;#if CONFIG_USE_SERVER_AEC// 若啟用了服務器端 AEC,并且任務中包含有效時間戳,則記錄該時間戳if (task->timestamp > 0) {lock.lock(); // 重新加鎖以保護 timestamp_queue_timestamp_queue_.push_back(task->timestamp);}#endif}// 最后,任務退出時打印日志ESP_LOGW(TAG, "Audio output task stopped");
}
void AudioService::OpusCodecTask() {while (true) {// 加鎖并等待條件滿足:// - 服務已停止// - 編碼隊列非空 且 發送隊列未滿// - 解碼隊列非空 且 播放隊列未滿std::unique_lock<std::mutex> lock(audio_queue_mutex_);audio_queue_cv_.wait(lock, [this]() {return service_stopped_ ||(!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) ||(!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE);});// 若服務已停止,則退出任務if (service_stopped_) {break;}/** ========================* 解碼邏輯* ======================== */if (!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE) {// 取出一個待解碼數據包auto packet = std::move(audio_decode_queue_.front());audio_decode_queue_.pop_front();audio_queue_cv_.notify_all();lock.unlock(); // 解鎖以便其他線程訪問隊列// 構造新的播放任務auto task = std::make_unique<AudioTask>();task->type = kAudioTaskTypeDecodeToPlaybackQueue;task->timestamp = packet->timestamp;// 設置解碼參數SetDecodeSampleRate(packet->sample_rate, packet->frame_duration);// 解碼數據if (opus_decoder_->Decode(std::move(packet->payload), task->pcm)) {// 如果解碼后的采樣率不一致,則重采樣if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {int target_size = output_resampler_.GetOutputSamples(task->pcm.size());std::vector<int16_t> resampled(target_size);output_resampler_.Process(task->pcm.data(), task->pcm.size(), resampled.data());task->pcm = std::move(resampled);}// 加鎖并推送到播放隊列lock.lock();audio_playback_queue_.push_back(std::move(task));audio_queue_cv_.notify_all();} else {// 解碼失敗ESP_LOGE(TAG, "Failed to decode audio");lock.lock();}debug_statistics_.decode_count++;}/** ========================* 編碼邏輯* ======================== */if (!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) {auto task = std::move(audio_encode_queue_.front());audio_encode_queue_.pop_front();audio_queue_cv_.notify_all();lock.unlock(); // 解鎖以進行編碼// 構建音頻流數據包auto packet = std::make_unique<AudioStreamPacket>();packet->frame_duration = OPUS_FRAME_DURATION_MS;packet->sample_rate = 16000;packet->timestamp = task->timestamp;// 編碼 PCM 數據if (!opus_encoder_->Encode(std::move(task->pcm), packet->payload)) {ESP_LOGE(TAG, "Failed to encode audio");continue;}// 根據任務類型,推送到不同隊列if (task->type == kAudioTaskTypeEncodeToSendQueue) {{std::lock_guard<std::mutex> lock(audio_queue_mutex_);audio_send_queue_.push_back(std::move(packet));}// 通知有新的可發送數據if (callbacks_.on_send_queue_available) {callbacks_.on_send_queue_available();}} else if (task->type == kAudioTaskTypeEncodeToTestingQueue) {std::lock_guard<std::mutex> lock(audio_queue_mutex_);audio_testing_queue_.push_back(std::move(packet));}debug_statistics_.encode_count++;lock.lock(); // 重新加鎖以進入下一輪循環}}// 任務退出時記錄日志ESP_LOGW(TAG, "Opus codec task stopped");
}
?