AlwaysOnHotwordDetector.java revision 668327d0286591324fa7592ee9b39255076e2165
1/**
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.service.voice;
18
19import android.content.Intent;
20import android.hardware.soundtrigger.IRecognitionStatusCallback;
21import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
22import android.hardware.soundtrigger.KeyphraseMetadata;
23import android.hardware.soundtrigger.SoundTrigger;
24import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel;
25import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
26import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
27import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
28import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
29import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
30import android.os.AsyncTask;
31import android.os.Handler;
32import android.os.Message;
33import android.os.RemoteException;
34import android.util.Slog;
35
36import com.android.internal.app.IVoiceInteractionManagerService;
37
38/**
39 * A class that lets a VoiceInteractionService implementation interact with
40 * always-on keyphrase detection APIs.
41 */
42public class AlwaysOnHotwordDetector {
43    //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----//
44    /**
45     * Indicates that this hotword detector is no longer valid for any recognition
46     * and should not be used anymore.
47     */
48    private static final int STATE_INVALID = -3;
49
50    /**
51     * Indicates that recognition for the given keyphrase is not available on the system
52     * because of the hardware configuration.
53     */
54    public static final int STATE_HARDWARE_UNAVAILABLE = -2;
55    /**
56     * Indicates that recognition for the given keyphrase is not supported.
57     */
58    public static final int STATE_KEYPHRASE_UNSUPPORTED = -1;
59    /**
60     * Indicates that the given keyphrase is not enrolled.
61     */
62    public static final int STATE_KEYPHRASE_UNENROLLED = 1;
63    /**
64     * Indicates that the given keyphrase is currently enrolled and it's possible to start
65     * recognition for it.
66     */
67    public static final int STATE_KEYPHRASE_ENROLLED = 2;
68
69    /**
70     * Indicates that the detector isn't ready currently.
71     */
72    private static final int STATE_NOT_READY = 0;
73
74    // Keyphrase management actions. Used in getManageIntent() ----//
75    /** Indicates that we need to enroll. */
76    public static final int MANAGE_ACTION_ENROLL = 0;
77    /** Indicates that we need to re-enroll. */
78    public static final int MANAGE_ACTION_RE_ENROLL = 1;
79    /** Indicates that we need to un-enroll. */
80    public static final int MANAGE_ACTION_UN_ENROLL = 2;
81
82    //-- Flags for startRecogntion    ----//
83    /** Empty flag for {@link #startRecognition(int)}. */
84    public static final int RECOGNITION_FLAG_NONE = 0;
85    /**
86     * Recognition flag for {@link #startRecognition(int)} that indicates
87     * whether the trigger audio for hotword needs to be captured.
88     */
89    public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;
90
91    //---- Recognition mode flags. Return codes for getSupportedRecognitionModes() ----//
92    // Must be kept in sync with the related attribute defined as searchKeyphraseRecognitionFlags.
93
94    /**
95     * Simple recognition of the key phrase. Returned by {@link #getSupportedRecognitionModes()}
96     */
97    public static final int RECOGNITION_MODE_VOICE_TRIGGER
98            = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;
99    /**
100     * Trigger only if one user is identified. Returned by {@link #getSupportedRecognitionModes()}
101     */
102    public static final int RECOGNITION_MODE_USER_IDENTIFICATION
103            = SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION;
104
105    static final String TAG = "AlwaysOnHotwordDetector";
106    // TODO: Set to false.
107    static final boolean DBG = true;
108
109    private static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
110    private static final int STATUS_OK = SoundTrigger.STATUS_OK;
111
112    private static final int MSG_STATE_CHANGED = 1;
113    private static final int MSG_HOTWORD_DETECTED = 2;
114    private static final int MSG_DETECTION_STARTED = 3;
115    private static final int MSG_DETECTION_STOPPED = 4;
116    private static final int MSG_DETECTION_ERROR = 5;
117
118    private static final int FLAG_REQUESTED = 0x1;
119    private static final int FLAG_STARTED = 0x2;
120    private static final int FLAG_CALL_ACTIVE = 0x4;
121    private static final int FLAG_MICROPHONE_OPEN = 0x8;
122
123    private final String mText;
124    private final String mLocale;
125    /**
126     * The metadata of the Keyphrase, derived from the enrollment application.
127     * This may be null if this keyphrase isn't supported by the enrollment application.
128     */
129    private final KeyphraseMetadata mKeyphraseMetadata;
130    private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
131    private final IVoiceInteractionService mVoiceInteractionService;
132    private final IVoiceInteractionManagerService mModelManagementService;
133    private final SoundTriggerListener mInternalCallback;
134    private final Callback mExternalCallback;
135    private final Object mLock = new Object();
136    private final Handler mHandler;
137
138    private int mAvailability = STATE_NOT_READY;
139    private int mInternalState = 0;
140    private int mRecognitionFlags = RECOGNITION_FLAG_NONE;
141
142    /**
143     * Callbacks for always-on hotword detection.
144     */
145    public interface Callback {
146        /**
147         * Called when the hotword availability changes.
148         * This indicates a change in the availability of recognition for the given keyphrase.
149         * It's called at least once with the initial availability.<p/>
150         *
151         * Availability implies whether the hardware on this system is capable of listening for
152         * the given keyphrase or not. <p/>
153         * If the return code is one of {@link #STATE_HARDWARE_UNAVAILABLE} or
154         * {@link #STATE_KEYPHRASE_UNSUPPORTED},
155         * detection is not possible and no further interaction should be
156         * performed with this detector. <br/>
157         * If it is {@link #STATE_KEYPHRASE_UNENROLLED} the caller may choose to begin
158         * an enrollment flow for the keyphrase. <br/>
159         * and for {@link #STATE_KEYPHRASE_ENROLLED} a recognition can be started as desired. <p/>
160         */
161        void onAvailabilityChanged(int status);
162        /**
163         * Called when the keyphrase is spoken.
164         * This implicitly stops listening for the keyphrase once it's detected.
165         * Clients should start a recognition again once they are done handling this
166         * detection.
167         *
168         * @param data Optional trigger audio data, if it was requested during
169         *        {@link AlwaysOnHotwordDetector#startRecognition(int)}.
170         */
171        void onDetected(byte[] data);
172        /**
173         * Called when the detection for the associated keyphrase starts.
174         * This is called as a result of a successful call to
175         * {@link AlwaysOnHotwordDetector#startRecognition(int)}.
176         */
177        void onDetectionStarted();
178        /**
179         * Called when the detection for the associated keyphrase stops.
180         * This is called as a result of a successful call to
181         * {@link AlwaysOnHotwordDetector#stopRecognition()}.
182         */
183        void onDetectionStopped();
184        /**
185         * Called when the detection fails due to an error.
186         */
187        void onError();
188    }
189
190    /**
191     * @param text The keyphrase text to get the detector for.
192     * @param locale The java locale for the detector.
193     * @param callback A non-null Callback for receiving the recognition events.
194     * @param voiceInteractionService The current voice interaction service.
195     * @param modelManagementService A service that allows management of sound models.
196     *
197     * @hide
198     */
199    public AlwaysOnHotwordDetector(String text, String locale, Callback callback,
200            KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
201            IVoiceInteractionService voiceInteractionService,
202            IVoiceInteractionManagerService modelManagementService) {
203        mText = text;
204        mLocale = locale;
205        mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
206        mKeyphraseMetadata = mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale);
207        mExternalCallback = callback;
208        mHandler = new MyHandler();
209        mInternalCallback = new SoundTriggerListener(mHandler);
210        mVoiceInteractionService = voiceInteractionService;
211        mModelManagementService = modelManagementService;
212        new RefreshAvailabiltyTask().execute();
213    }
214
215    /**
216     * Gets the recognition modes supported by the associated keyphrase.
217     *
218     * @throws UnsupportedOperationException if the keyphrase itself isn't supported.
219     *         Callers should only call this method after a supported state callback on
220     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
221     * @throws IllegalStateException if the detector is in an invalid state.
222     *         This may happen if another detector has been instantiated or the
223     *         {@link VoiceInteractionService} hosting this detector has been shut down.
224     */
225    public int getSupportedRecognitionModes() {
226        synchronized (mLock) {
227            return getSupportedRecognitionModesLocked();
228        }
229    }
230
231    private int getSupportedRecognitionModesLocked() {
232        if (mAvailability == STATE_INVALID) {
233            throw new IllegalStateException(
234                    "getSupportedRecognitionModes called on an invalid detector");
235        }
236
237        // This method only makes sense if we can actually support a recognition.
238        if (mAvailability != STATE_KEYPHRASE_ENROLLED
239                && mAvailability != STATE_KEYPHRASE_UNENROLLED) {
240            throw new UnsupportedOperationException(
241                    "Getting supported recognition modes for the keyphrase is not supported");
242        }
243
244        return mKeyphraseMetadata.recognitionModeFlags;
245    }
246
247    /**
248     * Starts recognition for the associated keyphrase.
249     *
250     * @param recognitionFlags The flags to control the recognition properties.
251     *        The allowed flags are {@link #RECOGNITION_FLAG_NONE} and
252     *        {@link #RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO}.
253     * @throws UnsupportedOperationException if the recognition isn't supported.
254     *         Callers should only call this method after a supported state callback on
255     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
256     * @throws IllegalStateException if the detector is in an invalid state.
257     *         This may happen if another detector has been instantiated or the
258     *         {@link VoiceInteractionService} hosting this detector has been shut down.
259     */
260    public void startRecognition(int recognitionFlags) {
261        synchronized (mLock) {
262            if (mAvailability == STATE_INVALID) {
263                throw new IllegalStateException("startRecognition called on an invalid detector");
264            }
265
266            // Check if we can start/stop a recognition.
267            if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
268                throw new UnsupportedOperationException(
269                        "Recognition for the given keyphrase is not supported");
270            }
271
272            mInternalState |= FLAG_REQUESTED;
273            mRecognitionFlags = recognitionFlags;
274            updateRecognitionLocked();
275        }
276    }
277
278    /**
279     * Stops recognition for the associated keyphrase.
280     *
281     * @throws UnsupportedOperationException if the recognition isn't supported.
282     *         Callers should only call this method after a supported state callback on
283     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
284     * @throws IllegalStateException if the detector is in an invalid state.
285     *         This may happen if another detector has been instantiated or the
286     *         {@link VoiceInteractionService} hosting this detector has been shut down.
287     */
288    public void stopRecognition() {
289        synchronized (mLock) {
290            if (mAvailability == STATE_INVALID) {
291                throw new IllegalStateException("stopRecognition called on an invalid detector");
292            }
293
294            // Check if we can start/stop a recognition.
295            if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
296                throw new UnsupportedOperationException(
297                        "Recognition for the given keyphrase is not supported");
298            }
299
300            mInternalState &= ~FLAG_REQUESTED;
301            mRecognitionFlags = RECOGNITION_FLAG_NONE;
302            updateRecognitionLocked();
303        }
304    }
305
306    /**
307     * Gets an intent to manage the associated keyphrase.
308     *
309     * @param action The manage action that needs to be performed.
310     *        One of {@link #MANAGE_ACTION_ENROLL}, {@link #MANAGE_ACTION_RE_ENROLL} or
311     *        {@link #MANAGE_ACTION_UN_ENROLL}.
312     * @return An {@link Intent} to manage the given keyphrase.
313     * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
314     *         Callers should only call this method after a supported state callback on
315     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
316     * @throws IllegalStateException if the detector is in an invalid state.
317     *         This may happen if another detector has been instantiated or the
318     *         {@link VoiceInteractionService} hosting this detector has been shut down.
319     */
320    public Intent getManageIntent(int action) {
321        synchronized (mLock) {
322            return getManageIntentLocked(action);
323        }
324    }
325
326    private Intent getManageIntentLocked(int action) {
327        if (mAvailability == STATE_INVALID) {
328            throw new IllegalStateException("getManageIntent called on an invalid detector");
329        }
330
331        // This method only makes sense if we can actually support a recognition.
332        if (mAvailability != STATE_KEYPHRASE_ENROLLED
333                && mAvailability != STATE_KEYPHRASE_UNENROLLED) {
334            throw new UnsupportedOperationException(
335                    "Managing the given keyphrase is not supported");
336        }
337
338        if (action != MANAGE_ACTION_ENROLL
339                && action != MANAGE_ACTION_RE_ENROLL
340                && action != MANAGE_ACTION_UN_ENROLL) {
341            throw new IllegalArgumentException("Invalid action specified " + action);
342        }
343
344        return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale);
345    }
346
347    /**
348     * Invalidates this hotword detector so that any future calls to this result
349     * in an IllegalStateException.
350     *
351     * @hide
352     */
353    void invalidate() {
354        synchronized (mLock) {
355            mAvailability = STATE_INVALID;
356            notifyStateChangedLocked();
357        }
358    }
359
360    /**
361     * Reloads the sound models from the service.
362     *
363     * @hide
364     */
365    void onSoundModelsChanged() {
366        synchronized (mLock) {
367            // TODO: This should stop the recognition if it was using an enrolled sound model
368            // that's no longer available.
369            if (mAvailability == STATE_INVALID
370                    || mAvailability == STATE_HARDWARE_UNAVAILABLE
371                    || mAvailability == STATE_KEYPHRASE_UNSUPPORTED) {
372                Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config");
373                return;
374            }
375
376            // Execute a refresh availability task - which should then notify of a change.
377            new RefreshAvailabiltyTask().execute();
378        }
379    }
380
381    @SuppressWarnings("unused")
382    private void onCallStateChanged(boolean active) {
383        synchronized (mLock) {
384            if (active) {
385                mInternalState |= FLAG_CALL_ACTIVE;
386            } else {
387                mInternalState &= ~FLAG_CALL_ACTIVE;
388            }
389
390            updateRecognitionLocked();
391        }
392    }
393
394    @SuppressWarnings("unused")
395    private void onMicrophoneStateChanged(boolean open) {
396        synchronized (mLock) {
397            if (open) {
398                mInternalState |= FLAG_MICROPHONE_OPEN;
399            } else {
400                mInternalState &= ~FLAG_MICROPHONE_OPEN;
401            }
402
403            updateRecognitionLocked();
404        }
405    }
406
407    private void updateRecognitionLocked() {
408        // Don't attempt to update the recognition state if keyphrase isn't enrolled.
409        if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
410            return;
411        }
412
413        // Start recognition if requested and not in a call/reading from the microphone
414        boolean start = (mInternalState&FLAG_REQUESTED) != 0
415                && (mInternalState&FLAG_CALL_ACTIVE) == 0
416                && (mInternalState&FLAG_MICROPHONE_OPEN) == 0;
417        boolean requested = (mInternalState&FLAG_REQUESTED) != 0;
418
419        if (start && (mInternalState&FLAG_STARTED) == 0) {
420            // Start recognition.
421            if (DBG) Slog.d(TAG, "starting recognition...");
422            int status = startRecognitionLocked();
423            if (status == STATUS_OK) {
424                mHandler.sendEmptyMessage(MSG_DETECTION_STARTED);
425            } else {
426                if (DBG) Slog.d(TAG, "failed to start recognition: " + status);
427                mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
428            }
429            // Post the callback
430            return;
431        }
432
433        if (!start && (mInternalState&FLAG_STARTED) != 0) {
434            // Stop recognition
435            // Only notify the callback if a recognition was *not* requested.
436            // For internal stoppages, don't notify the callback.
437            if (DBG) Slog.d(TAG, "stopping recognition...");
438            int status = stopRecognitionLocked();
439            if (status == STATUS_OK) {
440                if (!requested) mHandler.sendEmptyMessage(MSG_DETECTION_STOPPED);
441            } else {
442                if (!requested) mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
443                if (DBG) Slog.d(TAG, "failed to stop recognition: " + status);
444            }
445            return;
446        }
447    }
448
449    private int startRecognitionLocked() {
450        KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1];
451        // TODO: Do we need to do something about the confidence level here?
452        recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.id,
453                mKeyphraseMetadata.recognitionModeFlags, new ConfidenceLevel[0]);
454        boolean captureTriggerAudio =
455                (mRecognitionFlags&RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0;
456        int code = STATUS_ERROR;
457        try {
458            code = mModelManagementService.startRecognition(mVoiceInteractionService,
459                    mKeyphraseMetadata.id, mInternalCallback,
460                    new RecognitionConfig(
461                            captureTriggerAudio, recognitionExtra, null /* additional data */));
462        } catch (RemoteException e) {
463            Slog.w(TAG, "RemoteException in startRecognition!");
464        }
465        if (code != STATUS_OK) {
466            Slog.w(TAG, "startRecognition() failed with error code " + code);
467        }
468        return code;
469    }
470
471    private int stopRecognitionLocked() {
472        int code = STATUS_ERROR;
473        try {
474            code = mModelManagementService.stopRecognition(
475                    mVoiceInteractionService, mKeyphraseMetadata.id, mInternalCallback);
476        } catch (RemoteException e) {
477            Slog.w(TAG, "RemoteException in stopRecognition!");
478        }
479
480        if (code != STATUS_OK) {
481            Slog.w(TAG, "stopRecognition() failed with error code " + code);
482        }
483        return code;
484    }
485
486    private void notifyStateChangedLocked() {
487        Message message = Message.obtain(mHandler, MSG_STATE_CHANGED);
488        message.arg1 = mAvailability;
489        message.sendToTarget();
490    }
491
492    /** @hide */
493    static final class SoundTriggerListener extends IRecognitionStatusCallback.Stub {
494        private final Handler mHandler;
495
496        public SoundTriggerListener(Handler handler) {
497            mHandler = handler;
498        }
499
500        @Override
501        public void onDetected(KeyphraseRecognitionEvent event) {
502            Slog.i(TAG, "onDetected");
503            Message message = Message.obtain(mHandler, MSG_HOTWORD_DETECTED);
504            message.obj = event.data;
505            message.sendToTarget();
506        }
507
508        @Override
509        public void onError(int status) {
510            Slog.i(TAG, "onError: " + status);
511            mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
512        }
513    }
514
515    class MyHandler extends Handler {
516        @Override
517        public void handleMessage(Message msg) {
518            synchronized (mLock) {
519                if (mAvailability == STATE_INVALID) {
520                    Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector");
521                    return;
522                }
523            }
524
525            switch (msg.what) {
526                case MSG_STATE_CHANGED:
527                    mExternalCallback.onAvailabilityChanged(msg.arg1);
528                    break;
529                case MSG_HOTWORD_DETECTED:
530                    synchronized (mLock) {
531                        mInternalState &= ~FLAG_REQUESTED;
532                        mInternalState &= ~FLAG_STARTED;
533                    }
534                    mExternalCallback.onDetected((byte[]) msg.obj);
535                    break;
536                case MSG_DETECTION_STARTED:
537                    synchronized (mLock) {
538                        mInternalState |= FLAG_STARTED;
539                    }
540                    mExternalCallback.onDetectionStarted();
541                    break;
542                case MSG_DETECTION_STOPPED:
543                    synchronized (mLock) {
544                        mInternalState &= ~FLAG_REQUESTED;
545                        mInternalState &= ~FLAG_STARTED;
546                    }
547                    mExternalCallback.onDetectionStopped();
548                    break;
549                case MSG_DETECTION_ERROR:
550                    synchronized (mLock) {
551                        mInternalState &= ~FLAG_REQUESTED;
552                        mInternalState &= ~FLAG_STARTED;
553                    }
554                    mExternalCallback.onError();
555                    break;
556                default:
557                    super.handleMessage(msg);
558            }
559        }
560    }
561
562    class RefreshAvailabiltyTask extends AsyncTask<Void, Void, Void> {
563
564        @Override
565        public Void doInBackground(Void... params) {
566            int availability = internalGetInitialAvailability();
567            boolean enrolled = false;
568            // Fetch the sound model if the availability is one of the supported ones.
569            if (availability == STATE_NOT_READY
570                    || availability == STATE_KEYPHRASE_UNENROLLED
571                    || availability == STATE_KEYPHRASE_ENROLLED) {
572                enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id);
573                if (!enrolled) {
574                    availability = STATE_KEYPHRASE_UNENROLLED;
575                } else {
576                    availability = STATE_KEYPHRASE_ENROLLED;
577                }
578            }
579
580            synchronized (mLock) {
581                if (DBG) {
582                    Slog.d(TAG, "Hotword availability changed from " + mAvailability
583                            + " -> " + availability);
584                }
585                mAvailability = availability;
586                notifyStateChangedLocked();
587            }
588            return null;
589        }
590
591        /**
592         * @return The initial availability without checking the enrollment status.
593         */
594        private int internalGetInitialAvailability() {
595            synchronized (mLock) {
596                // This detector has already been invalidated.
597                if (mAvailability == STATE_INVALID) {
598                    return STATE_INVALID;
599                }
600            }
601
602            ModuleProperties dspModuleProperties = null;
603            try {
604                dspModuleProperties =
605                        mModelManagementService.getDspModuleProperties(mVoiceInteractionService);
606            } catch (RemoteException e) {
607                Slog.w(TAG, "RemoteException in getDspProperties!");
608            }
609            // No DSP available
610            if (dspModuleProperties == null) {
611                return STATE_HARDWARE_UNAVAILABLE;
612            }
613            // No enrollment application supports this keyphrase/locale
614            if (mKeyphraseMetadata == null) {
615                return STATE_KEYPHRASE_UNSUPPORTED;
616            }
617            return STATE_NOT_READY;
618        }
619
620        /**
621         * @return The corresponding {@link KeyphraseSoundModel} or null if none is found.
622         */
623        private boolean internalGetIsEnrolled(int keyphraseId) {
624            try {
625                return mModelManagementService.isEnrolledForKeyphrase(
626                        mVoiceInteractionService, keyphraseId);
627            } catch (RemoteException e) {
628                Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!");
629            }
630            return false;
631        }
632    }
633}
634