語音喚醒實現

Ranchok發表於2019-05-05

現在無論是智慧音響還是手機或者平板,都可以在不管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);
                }
            }
        }

    };
複製程式碼

相關文章