現在無論是智慧音響還是手機或者平板,都可以在不管cpu是否休眠的時候喊一聲“小愛同學”或者“小布小布”就可以喚醒了。那麼哪些實現是比較好的呢,或者存在哪些未知的坑呢?今年從過完年後就一直在做語音喚醒這塊,一款好的,有價值的專案,會有很多的問題需要去解決。
1.前言
這裡先丟擲幾個問題。
1.語音喚醒的實現放在android哪一層處理會比較好?
肯定是放在系統的framework層,應用存在各種crash以及被強殺等,及時是系統應用,其存活完全不能和系統服務相比。
2.需不需要回聲消除去處理音訊?
這個也是需要的,AEC可以比較大的降低誤喚醒率影響。這裡我吐槽下我的小米mix2s手機,這個誤喚醒率實在太高了。看視訊的時候經常誤喚醒,及其煩人,所以我把整個語音喚醒都關了。我們是把AEC放在hal層下,通過AudioRecord提供到上層實現
3.為了降低誤喚醒,還有哪些比較好的辦法?
可以對一級喚醒後的喚醒詞做二級校驗,降低誤喚醒率。
4.ASR放在哪一層處理?
這個放在應用層做識別處理即可
5.framework層如何拉起應用?
喚醒後,拉起應用應該快速,如果等了2,3秒,螢幕才亮起來,這個體驗就會很差,所以不建議使用廣播的方式,因為framework層通過廣播通知應用起來的話,在連線wifi或者開機時,系統會收到大量的廣播事件,android會一個一個處理廣播事件,這裡會導致應用起來的比較慢。所以可以選擇直接拉起應用的activity或者service來實現拉起應用。
Slog.d(TAG, "notifyWakeup");
Intent intent = new Intent();
...
...
intent.putExtra("topPackage", getTopPackage());
intent.putExtra("topActivity", getTopActivity());
mContext.startService(intent);
複製程式碼
2.喚醒方案實現
2.1.低功耗語音喚醒方案實現
所謂的低功耗語音喚醒方案無非就是加語音喚醒晶片,確保在cpu休眠的時候它還在繼續工作,當使用者說喚醒詞時,會將喚醒事件通知到framework層,從而喚醒cpu,觸發整個喚醒流程。
但是低功耗喚醒方案存在的問題是,如果不加以對喚醒詞的資料校驗的話誤喚醒率相對較高。這樣就會讓使用者很難接受。所以需要加二級喚醒校驗,譬如如果使用dspg的話,會從soundtrigger獲取到喚醒詞資料,再將喚醒詞資料送給二級語音模型訓練庫,從而降低喚醒率。
private SoundTriggerDetector.Callback mSoundTriggerDetectorCallback = new SoundTriggerDetector.Callback() {
@Override
public void onAvailabilityChanged(int status) {
Slog.d(TAG, "SoundTriggerDetectorCallback onAvailabilityChanged status=" + status);
}
@Override
public void onDetected(SoundTriggerDetector.EventPayload eventPayload) {
Slog.d(TAG, "SoundTriggerDetectorCallback onDetected");
if (mAuthDUI.isAvailable() && startWakeupApp()) {
byte[] triggeraudio = eventPayload.getTriggerAudio();
if (triggeraudio != null) {
mSingleEngine.feed(triggeraudio, triggeraudio.length);
Slog.d(TAG, "feed trigger to single engine end");
}
}
}
@Override
public void onError() {
Slog.d(TAG, "SoundTriggerDetectorCallback onError");
}
@Override
public void onRecognitionPaused() {
Slog.d(TAG, "SoundTriggerDetectorCallback onRecognitionPaused");
}
@Override
public void onRecognitionResumed() {
Slog.d(TAG, "SoundTriggerDetectorCallback onRecognitionResumed");
}
};
複製程式碼
2.2.二級喚醒實現
二級喚醒的實現其實就是在cpu沒有休眠,或者沒有滅屏的時候通過audiorecord獲取音訊資料送給語音喚醒模型庫來實現。如果是在cpu沒有休眠時,走二級喚醒,可以讓核心加個事件上報機制,告訴framework層系統沒有休眠,繼續二級喚醒,這裡需要說下為什麼要走二級喚醒?因為二級喚醒會有AEC,會降低誤喚醒率。
二級喚醒的實現其實也就是在framework層開一個錄音機,在系統服務一起來的時候就開啟錄音機,然後只要沒有切換到cpu休眠狀態時,都走二級喚醒,錄音機不斷地將喚醒資料餵給語音喚醒訓練庫。當檢測到有喚醒事件時,可以選擇拉起service的方式拉起應用。
這裡在預言階段就測試了,在cpu沒有休眠的時候存在一個audiorecord的話,對功耗影響並不會很多,完全可以接受。這裡需要修改audio這塊,讓android支援多錄音,不然上層的app無法正常錄音。
private void startSingleRecord() {
startSingleRecord(MediaRecorder.AudioSource.VOICE_RECOGNITION, AudioFormat.CHANNEL_IN_MONO);
}
複製程式碼
需要注意的是多驗證audiorecord,防止存在建立了多個audiorecord但是沒有銷燬導致功耗降不下去的問題。
我一般都是直接dumpsys audio_flinger來檢視當前的錄音數量和pid。
adb shell dumpsys media.audio_flinger
複製程式碼
2.3.狀態切換中導致的喚不醒
在一級切換二級,或者二級切換一級過程中,會存在你喊“小布小布”時,第一個小布在二級中,第二個小布在一級中,導致一級和二級都沒有喚醒。所以如果在亮滅屏時判斷的時候,可以不要去監聽亮滅屏廣播去實現,直接在Notifier中去監聽亮滅屏,去避免切換的這200多毫秒。
private void handleEarlyInteractiveChange() {
synchronized (mLock) {
if (mInteractive) {
// Waking up...
mHandler.post(new Runnable() {
@Override
public void run() {
EventLog.writeEvent(EventLogTags.POWER_SCREEN_STATE, 1, 0, 0, 0);
mPolicy.startedWakingUp();
if (mAIManagerInternal != null) {
mAIManagerInternal.startedWakingUp();
} else {
Slog.d(TAG, "AIManagerInternal is null");
}
}
});
// Send interactive broadcast.
mPendingInteractiveState = INTERACTIVE_STATE_AWAKE;
mPendingWakeUpBroadcast = true;
updatePendingBroadcastLocked();
} else {
// Going to sleep...
// Tell the policy that we started going to sleep.
final int why = translateOffReason(mInteractiveChangeReason);
mHandler.post(new Runnable() {
@Override
public void run() {
mPolicy.startedGoingToSleep(why);
if (mAIManagerInternal != null) {
mAIManagerInternal.startedGoingToSleep(why);
} else {
Slog.d(TAG, "AIManagerInternal is null");
}
}
});
}
}
}
複製程式碼
在自定義系統服務中去實現這倆個函式。
final class LocalService extends AIManagerInternal {
@Override
public void startedGoingToSleep(int why) {
synchronized (AIManagerService.this) {
AIManagerService.this.startedGoingToSleep(why);
}
}
@Override
public void startedWakingUp() {
synchronized (AIManagerService.this) {
AIManagerService.this.startedWakingUp();
}
}
}
複製程式碼
3.如何避免丟音
當你通過語音去喚醒時,如果你快速的說,譬如“小布小布,開啟設定”。不管在亮屏還是滅屏的時候可能都會存在丟音,最後送給到上層去做識別的音訊資料只有“設定”,那麼如何避免丟音呢。做快取,無論是一級喚醒還是二級喚醒,framework層都應該做音訊資料的快取,快取足夠的資料,不管應用什麼時候來取,都應該快取足量的資料,確保避免丟音問題的存在。
private ArrayBlockingQueue<byte[]> mBlockingQueue;
...
...
mBlockingQueue = new ArrayBlockingQueue<>(1024);
複製程式碼
對於dspg,則應該做oneshot來快取喚醒點前面1-2秒的音訊資料,確保在從dspg喚醒後,開一個audiorecord拿到的音訊資料是快取了喚醒點前1-2秒的。
4.其他點
整個語音喚醒中還有很多點和坑,以上是目前想到的幾點。
下面是專案中用到的一些地方,方便借鑑。
1.判斷系統有無應用在錄音,譬如在滅屏但是有應用在錄音時,這個時候可以不用切成一級喚醒,為了充分利用aec,可以走二級喚醒,如何去判斷應用錄音以及錄音結束後收到通知呢?
判斷系統中有無應用錄音
public boolean isAppRecording() {
List<AudioRecordingConfiguration> configs = mAudioManager.getActiveRecordingConfigurations();
Slog.d(TAG, "recording configs.size=" + configs.size());
if (configs.size() > 1) {
Slog.d(TAG, "has other app recording");
return true;
}
Slog.d(TAG, "no app is recording");
return false;
}
複製程式碼
註冊監聽,在錄音狀態改變是收到回撥通知
AudioManager.AudioRecordingCallback mAudioRecordingCallback = new AudioManager.AudioRecordingCallback() {
@Override
public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
super.onRecordingConfigChanged(configs);
Slog.d(TAG, "onRecordingConfigChanged configs.size:" + configs.size());
if (configs.size() == 1 && !isScreenOn()) {
Slog.d(TAG, "onRecordingConfigChanged switch to dspg wakeup");
//stop software wakeup,start dspg wakeup
mWorkHandler.removeMessages(MSG_SWITCH_TO_SINGLEOFF_MIC);
mWorkHandler.sendEmptyMessage(MSG_SWITCH_TO_SINGLEOFF_MIC);
}
}
};
複製程式碼
判斷系統中有無應用在放音
public boolean isAudioPlayback() {
List<AudioPlaybackConfiguration> configs = mAudioManager.getActivePlaybackConfigurations();
for (AudioPlaybackConfiguration config : configs) {
if (config.isActive()) {
Slog.d(TAG, "has other app audioplaing,pid is:" + config.getClientPid());
return true;
}
}
Slog.d(TAG, "no app audio is playing");
return false;
}
複製程式碼
應用放音結束時收到系統放音變化的註冊回撥
AudioManager.AudioPlaybackCallback mAudioPlaybackCallback = new AudioManager.AudioPlaybackCallback() {
@Override
public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
super.onPlaybackConfigChanged(configs);
{
boolean active = false;
Slog.d(TAG, "AudioPlaybackConfiguration.size" + configs.size());
for (AudioPlaybackConfiguration config : configs) {
if (config.isActive()) {
active = true;
}
}
Slog.d(TAG, "active:" + active);
if (!active && !isScreenOn()) {
Slog.d(TAG, "onPlaybackConfigChanged switch to dspg wakeup");
//stop aispeech wakeup,start dspg wakeup
mWorkHandler.removeMessages(MSG_SWITCH_TO_SINGLEOFF_MIC);
mWorkHandler.sendEmptyMessage(MSG_SWITCH_TO_SINGLEOFF_MIC);
}
if (active && !isScreenOn()) {
Slog.d(TAG, "onPlaybackConfigChanged switch to aispeech wakeup");
//stop dspg wakeup and start aispeech wakeup
mWorkHandler.removeMessages(MSG_SWITCH_TO_SINGLEON_MIC);
mWorkHandler.sendEmptyMessage(MSG_SWITCH_TO_SINGLEON_MIC);
}
}
}
};
複製程式碼