Android原生SpeechRecognizer(语音识别)

开篇先吐槽下,在Android 平台开发原生的SpeechRecognizer真是难受的,不像ios,无比轻松,平台统一。
由于Android 平台的碎片化问题比较严重,各个厂商都有自己的实现,尤其是语音助手出来以后,每家的语音服务肯定是不一样的。

目前Android原生的SpeechRecognizer做法应该有两种

  1. 默认调用原生SpeechRecognizer,并稍作修改
  2. 调用第三方,科大讯飞,百度等

这两种做法中

  • 1. 在Google原生系统是可以的,但是在国内的环境是需要修改,修改后能保证各个机型基本可以用,至于识别效果就要看各个机型自己实现的怎么样了
  • 2. 最简单省心省力,如果你的项目可以这么做,那么兄弟恭喜你,你是最幸福的

这里我们不讲第三方的,大家可以自己去集成第三方sdk,主要讨论原生的开发

​ 首先权限不要忘记(记得6.0以后动态请求权限)

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

​ 在SpeechRecognizer.class有这样SpeechRecognizer .isRecognitionAvailable一个方法

    /**
     * Checks whether a speech recognition service is available on the system. If this method
     * returns {@code false}, {@link SpeechRecognizer#createSpeechRecognizer(Context)} will
     * fail.
     * 
     * @param context with which {@code SpeechRecognizer} will be created
     * @return {@code true} if recognition is available, {@code false} otherwise
     */
    public static boolean isRecognitionAvailable(final Context context) {
        final List<ResolveInfo> list = context.getPackageManager().queryIntentServices(
                new Intent(RecognitionService.SERVICE_INTERFACE), 0);
        return list != null && list.size() != 0;
    }

​ 该方法在使用语音识别前建议要调用下,该方法是检查当前系统有没有语音识别服务,我相信绝大多数厂商都有这个服务,但是都有自己特别的实现,但是它至少有,有就可以用。但是,你像oppo的7.0以后机器,这个方法调用后就是false,这时候就是毫无办法了。oppo 7.0以后就是这样调用完后返回false,对于 oppo 这种情况,可以在手机上装一个讯飞语音+的app,语音识别就可以了,但是这种方法我估计没人会用,用户体验太差。

​ 如果该方法返回false在我们调用SpeechRecognizer.startListening();方法的时候会日志中发现这行log

no selected voice recognition service

​ 该日志在SpeechRecognizer.startListening(final Intent recognizerIntent)方法中,大家可以进源码查看这里就不贴了。

​ 检查完如果语音识别可用,接下来有两种做法我们一个个来


  1. 直接创建实例启动服务
mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
mSpeechRecognizer.setRecognitionListener(this);

​ 创建识别实例,并添加监听,这里没有什么问题,在监听中我们可以拿到我们的想要的回调

/**
 * Used for receiving notifications from the SpeechRecognizer when the
 * recognition related events occur. All the callbacks are executed on the
 * Application main thread.
 * 值的注意的是,所有的回调都在主线程
 */
public interface RecognitionListener {
    
    // 实例准备就绪 
    void onReadyForSpeech(Bundle params);

    // 开始语音识别
    void onBeginningOfSpeech();

    // 聆听分贝值 可能会有负数哦
    void onRmsChanged(float rmsdB);

    void onBufferReceived(byte[] buffer);

    // 识别结束
    void onEndOfSpeech();

    // 错误码
    void onError(int error);

    // 识别的结果,在某些国产机上,这个结果会是空
    void onResults(Bundle results);

    // 识别的部分结果 有些过程机上 [onResults] 方法为空,可以在这里拿到结果
    void onPartialResults(Bundle partialResults);

    void onEvent(int eventType, Bundle params);
}

​ 指的注意的是,如果 SpeechRecognizer .isRecognitionAvailable 方法返回false的话,即使注册了监听mSpeechRecognizer.setRecognitionListener(this);回调方法不会走的,因为没有该服务的

​ 下面就是样板代码了,都一样的

// 启动服务需要一个 Intent
mRecognitionIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
            mRecognitionIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
mRecognitionIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
mRecognitionIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3);
// mLocale 是一个语音种类,可以根据自己的需求去设置
mRecognitionIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, mLocale);

// 开始语音识别 结果在 mSpeechRecognizer.setRecognitionListener(this);回调中
mSpeechRecognizer.startListening(mRecognitionIntent);

// 停止监听
mSpeechRecognizer.stopListening();

// 取消服务
mSpeechRecognizer.cancel();

​ 在识别过程中,如果出错,错误码很有用

    // 错误码
    void onError(int error);

​ 这里是错误码的原因,可以做参考去排查,错误码在SpeechRecognizer.class中,可以自行查阅

    /** Network operation timed out. */
    public static final int ERROR_NETWORK_TIMEOUT = 1;

    /** Other network related errors. */
    public static final int ERROR_NETWORK = 2;

    /** Audio recording error. */
    public static final int ERROR_AUDIO = 3;

    /** Server sends error status. */
    public static final int ERROR_SERVER = 4;

    /** Other client side errors. */
    public static final int ERROR_CLIENT = 5;

    /** No speech input */
    public static final int ERROR_SPEECH_TIMEOUT = 6;

    /** No recognition result matched. */
    public static final int ERROR_NO_MATCH = 7;

    /** RecognitionService busy. */
    public static final int ERROR_RECOGNIZER_BUSY = 8;

    /** Insufficient permissions */
    public static final int ERROR_INSUFFICIENT_PERMISSIONS = 9;

​ 这里特别说明下,如果你所有操作都正常,可是在监听回调中一直出现 ERROR_INSUFFICIENT_PERMISSIONS 即错误码返回一直是 9,这时候可以尝试常看各个厂商的语音助手有没有在这里做处理,方法就是在说话的时候,这时打开语音助手,会有语音助手提示赋予应用权限,这种情况我在小米手机上遇到过,小爱同学需要打开权限,打开就好了。

​ 以上就是一般的做法,但是不一定有用,厂商会做什么事,我们是不知道滴,具体问题需要具体对待,下面我们来讨论另外一种实现


  1. 照旧在启动服务前需要检查服务是否存在 SpeechRecognizer.isRecognitionAvailable(context);如果返回false,要么歇菜(绝大多数不会出现),要么自己实现,我自己实现不了。

    如果返回true,说明有语音识别服务可以用,这时候我们需要记录下当前系统内置的是哪个服务

String serviceComponent = Settings.Secure.getString(context.getContentResolver(),
                                                            "voice_recognition_service");

serviceComponent就是我们的服务名称,eg:华为手机返回"com.huawei.vassistant/com.huawei.ziri.service.FakeRecognitionService"从名字看就是 FakeRecognitionService伪造的语音识别服务,就是说这个是不用的。这里多说下,华为使用的讯飞的语音识别服务。

​ 组装成组件

// 当前系统内置语音识别服务
ComponentName component = ComponentName.unflattenFromString(serviceComponent);

​ 组装成一个Component组件,后面我们需要用到


// 内置语音识别服务是否可用
boolean isRecognizerServiceValid = false;
ComponentName currentRecognitionCmp = null;
// 查找得到的 "可用的" 语音识别服务
List<ResolveInfo> list = context.getPackageManager().queryIntentServices(new Intent(RecognitionService.SERVICE_INTERFACE), MATCH_ALL);

if (list != null && list.size() != 0) {
    for (ResolveInfo info : list) {
        debugLog(TAG, "\t" + info.loadLabel(context.getPackageManager()) + ": "
                + info.serviceInfo.packageName + "/" + info.serviceInfo.name);
                
        // 这里拿系统使用的语音识别服务和内置的语音识别比较,如果相同,OK我们直接直接使用
        // 如果相同就可以直接使用mSpeechRecognizer =              SpeechRecognizer.createSpeechRecognizer(context);来创建实例,因为内置的可以使用
        if (info.serviceInfo.packageName.equals(component.getPackageName())) {
            isRecognizerServiceValid = true;
            break;
        } else {
         // 如果服务不同,说明 内置服务 和 系统使用 不是同一个,那么我们需要使用系统使用的
         // 因为内置的系统不用,我们用了也没有用
            currentRecognitionCmp = new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name);
        }

    }
} else {
    // 这里既是查不到可用的语音识别服务,可以歇菜了
    debugLog(TAG, "No recognition services installed");
    return false;
}

​ 根据判断结果创建实例

        // 当前系统内置语音识别服务可用
        if (isRecognizerServiceValid) {
            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
        } else {
          // 内置不可用,需要我们使用查找到的可用的
            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context, currentRecognitionCmp);
        }

        mSpeechRecognizer.setRecognitionListener(this);

​ 关于SpeechRecognizer createSpeechRecognizer(final Context context,final ComponentName serviceComponent)方法源码如下

    /**
     * Factory method to create a new {@code SpeechRecognizer}. Please note that
     * {@link #setRecognitionListener(RecognitionListener)} should be called before dispatching any
     * command to the created {@code SpeechRecognizer}, otherwise no notifications will be
     * received.
     *
     * Use this version of the method to specify a specific service to direct this
     * {@link SpeechRecognizer} to. Normally you would not use this; use
     * {@link #createSpeechRecognizer(Context)} instead to use the system default recognition
     * service.
     * 
     * @param context in which to create {@code SpeechRecognizer}
     * @param serviceComponent the {@link ComponentName} of a specific service to direct this
     *        {@code SpeechRecognizer} to
     * @return a new {@code SpeechRecognizer}
     */
    public static SpeechRecognizer createSpeechRecognizer(final Context context,
            final ComponentName serviceComponent) {
        if (context == null) {
            throw new IllegalArgumentException("Context cannot be null)");
        }
        checkIsCalledFromMainThread();
        return new SpeechRecognizer(context, serviceComponent);
    }

​ 注释写的很明白,不建议我们使用,但是没办法,我们也不想折腾,各大厂商自己有实现,只能这样了。

​ 使用该方法来做基本能满足大多数手机的功能实现,但是这种方法的前提有一个SpeechRecognizer.isRecognitionAvailable(final Context context)该方法要返回true,系统没有服务可用,那是没有办法的。

​ 以下是自己的实现,可以根据自己使用修改

        // 查找当前系统的内置使用的语音识别服务
        // com.huawei.vassistant/com.huawei.ziri.service.FakeRecognitionService
        String serviceComponent = Settings.Secure.getString(context.getContentResolver(),
                                                            "voice_recognition_service");

        debugLog(TAG, "voice_recognition_service : " + serviceComponent);

        if (TextUtils.isEmpty(serviceComponent)) {
            return false;
        }

        ComponentName component = ComponentName.unflattenFromString(serviceComponent);

        if (component == null) {
            debugLog(TAG, "voice_recognition_service component == null");
            return false;
        }

        debugLog(TAG, "serviceComponent : " + component.toShortString());

        boolean isRecognizerServiceValid = false;
        ComponentName currentRecognitionCmp = null;

        // 查找得到的 "可用的" 语音识别服务
        List<ResolveInfo> list = context.getPackageManager().queryIntentServices(new Intent(RecognitionService.SERVICE_INTERFACE), MATCH_ALL);
        if (list != null && list.size() != 0) {
            for (ResolveInfo info : list) {
                debugLog(TAG, "\t" + info.loadLabel(context.getPackageManager()) + ": "
                        + info.serviceInfo.packageName + "/" + info.serviceInfo.name);

                if (info.serviceInfo.packageName.equals(component.getPackageName())) {
                    isRecognizerServiceValid = true;
                    break;
                } else {
                    currentRecognitionCmp = new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name);
                }

            }
        } else {
            debugLog(TAG, "No recognition services installed");
            return false;
        }

        if (mSpeechRecognizer != null) {
            return true;
        }

        debugLog(TAG, "isRecognitionAvailable: " + SpeechRecognizer.isRecognitionAvailable(context));

        if (isRecognizerServiceValid) {
            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
        } else {
            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context, currentRecognitionCmp);
        }

        mSpeechRecognizer.setRecognitionListener(this);

        if (mRecognitionIntent == null) {
            mRecognitionIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
            mRecognitionIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
            mRecognitionIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
            mRecognitionIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3);
        }
        return true;

到这里之后就是监听回调,回到了第一种方法的实现。就不贴代码了。


如有错误,不吝赐教

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,348评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,122评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,936评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,427评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,467评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,785评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,931评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,696评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,141评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,483评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,625评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,291评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,892评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,492评论 2 348

推荐阅读更多精彩内容