Android 基于百度语音的语音交互功能

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/AmazingUU/article/details/53164553

项目里面用到了语音唤醒功能,前面一直在用讯飞的语音识别,本来打算也是直接用讯飞的语音唤醒,但是讯飞的语音唤醒要收费,试用版只有35天有效期。只好改用百度语音,百度语音所有功能免费,功能也比较简单实用,包括语音识别,语音合成和语音唤醒,正好可以组成一套完整的语音交互功能。

效果图:

这里写图片描述

首先是语音唤醒功能,说出关键词即可叫语音识别,唤醒成功会有语音提示,这里采用了百度语音的合成功能。然后百度语音识别会根据wifi情况自动切换在线或者离线识别,但是离线识别只能识别已经导入的关键词,而且离线第一次识别需要联网,识别成功,同样会有语音提示。效果图gif没有声音,Toast显示的时候就是语音提示的内容。

这里说一点,百度语音的demo里给的语音唤醒是在onResume()开始唤醒监听,唤醒成功后在onPause()里就停止唤醒监听。而我现在要在唤醒成功后弹出语音识别的UI界面,所以弹出UI的同时就会停止唤醒监听。如果语音识别成功,UI界面消失,唤醒监听会重新开始,此时说出唤醒词即可重新唤醒。但是如果识别失败,封装好的UI界面会变成下图情况,这时候就要手动点击重试或者取消才可以,不符合全语音交互的理念。为了解决这个情况,要将停止唤醒监听写到onStop()里,这样即使语音识别失败,也可以重新唤醒。

这里写图片描述

具体的集成步骤官方文档里都有,也可以参考下面的文章

http://blog.csdn.net/kjunchen/article/details/51093134

注:我这里语音识别和语音合成都用到了,所以官网下的两个sdk都要导入到工程里,这里还有个小问题,正常来说,Jar包导入到工程之后,还要将assert和jniLibs文件夹放到工程里,我这里只放了语音识别的assert文件夹,jniLibs文件夹我都没放入工程里,这样可以使用。如果我将语音识别和语音合成的assert和jniLibs都放到工程里,反而会报下面的错误,不知道为什么。

java.lang.UnsatisfiedLinkError: Native method not found: com.baidu.speech.easr.easrNativeJni.WakeUpFree:()I

MainActivity:

package com.example.administrator.baiduvoicetest;

import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.baidu.speech.EventListener;
import com.baidu.speech.EventManager;
import com.baidu.speech.EventManagerFactory;
import com.baidu.tts.auth.AuthInfo;
import com.baidu.tts.client.SpeechError;
import com.baidu.tts.client.SpeechSynthesizer;
import com.baidu.tts.client.SpeechSynthesizerListener;
import com.baidu.tts.client.TtsMode;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;

public class MainActivity extends AppCompatActivity {
    private TextView txtResult;
    private EditText mInput;

    private EventManager mWpEventManager;


    private SpeechSynthesizer mSpeechSynthesizer;
    private String mSampleDirPath;
    private static final String SAMPLE_DIR_NAME = "baiduTTS";
    private static final String SPEECH_FEMALE_MODEL_NAME = "bd_etts_speech_female.dat";
    private static final String SPEECH_MALE_MODEL_NAME = "bd_etts_speech_male.dat";
    private static final String TEXT_MODEL_NAME = "bd_etts_text.dat";
    private static final String LICENSE_FILE_NAME = "temp_license";
    private static final String ENGLISH_SPEECH_FEMALE_MODEL_NAME = "bd_etts_speech_female_en.dat";
    private static final String ENGLISH_SPEECH_MALE_MODEL_NAME = "bd_etts_speech_male_en.dat";
    private static final String ENGLISH_TEXT_MODEL_NAME = "bd_etts_text_en.dat";
    private static final String TAG = "MainActivity";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        txtResult = (TextView) findViewById(R.id.txtResult);
        txtResult.setText("请说唤醒词:  小度你好或者百度一下\n\n"+
                "离在线语法识别(首次使用需要联网授权)\n"+
                "语音识别开始后你可以说(可以根据语法自行定义离线说法):\n" +
                " 1. 打电话给张三(离线)\n" +
                " 2. 打电话给李四(离线)\n" +
                " 3. 打开计算器(离线)\n" +
                " 4. 明天天气怎么样(需要联网)\n" +
                " ..." +
                "\n");


        mInput= (EditText) findViewById(R.id.input);
        mInput.setVisibility(View.GONE);
        initialEnv();
        initialTts();
    }

    @Override
    protected void onResume() {
        super.onResume();

        // 唤醒功能打开步骤
        // 1) 创建唤醒事件管理器
        mWpEventManager = EventManagerFactory.create(MainActivity.this, "wp");

        // 2) 注册唤醒事件监听器
        mWpEventManager.registerListener(new EventListener() {
            @Override
            public void onEvent(String name, String params, byte[] data, int offset, int length) {
                Log.d(TAG, String.format("event: name=%s, params=%s", name, params));
                try {
                    JSONObject json = new JSONObject(params);
                    if ("wp.data".equals(name)) { // 每次唤醒成功, 将会回调name=wp.data的时间, 被激活的唤醒词在params的word字段
                        String word = json.getString("word");
                        txtResult.append("唤醒成功, 唤醒词: " + word + "\r\n");


                        mInput.setText("唤醒成功,请说出指令");
                        //mInput.setText("succeed");
                        Toast.makeText(MainActivity.this,mInput.getText(),Toast.LENGTH_LONG).show();
                        speak();
                        //延时3秒,防止语音合成的内容又被语音识别
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }


                        Intent intent = new Intent("com.baidu.action.RECOGNIZE_SPEECH");
                        intent.putExtra("grammar", "asset:///baidu_speech_grammardemo.bsg"); // 设置离线的授权文件(离线模块需要授权), 该语法可以用自定义语义工具生成, 链接http://yuyin.baidu.com/asr#m5
                        //intent.putExtra("slot-data", your slots); // 设置grammar中需要覆盖的词条,如联系人名
                        startActivityForResult(intent, 1);


                    } else if ("wp.exit".equals(name)) {
                        txtResult.append("唤醒已经停止: " + params + "\r\n");
                    }
                } catch (JSONException e) {
                    throw new AndroidRuntimeException(e);
                }
            }
        });

        // 3) 通知唤醒管理器, 启动唤醒功能
        HashMap params = new HashMap();
        params.put("kws-file", "assets:///WakeUpDemo.bin"); // 设置唤醒资源, 唤醒资源请到 http://yuyin.baidu.com/wake#m4 来评估和导出
        mWpEventManager.send("wp.start", new JSONObject(params).toString(), null, 0, 0);

    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode == RESULT_OK) {
            Bundle results = data.getExtras();
            ArrayList<String> results_recognition = results.getStringArrayList("results_recognition");
            //txtResult.append("识别结果(数组形式): " + results_recognition + "\n");

        //将数组形式的识别结果变为正常的String类型,例:[给张三打电话]变成给张三打电话
            String str=results_recognition+"";
            String res=str.substring(str.indexOf("[")+1,str.indexOf("]"));
            txtResult.append(res+"\n");


            mInput.setText("好的,马上执行"+res);
            speak();
            Toast.makeText(MainActivity.this,mInput.getText(),Toast.LENGTH_LONG).show();

        }
    }

    private void initialTts() {
        this.mSpeechSynthesizer = SpeechSynthesizer.getInstance();
        this.mSpeechSynthesizer.setContext(this);
        this.mSpeechSynthesizer.setSpeechSynthesizerListener(new SpeechSynthesizerListener() {
            @Override
            public void onSynthesizeStart(String s) {

            }

            @Override
            public void onSynthesizeDataArrived(String s, byte[] bytes, int i) {

            }

            @Override
            public void onSynthesizeFinish(String s) {

            }

            @Override
            public void onSpeechStart(String s) {

            }

            @Override
            public void onSpeechProgressChanged(String s, int i) {

            }

            @Override
            public void onSpeechFinish(String s) {

            }

            @Override
            public void onError(String s, SpeechError speechError) {

            }
        });
        // 文本模型文件路径 (离线引擎使用)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, mSampleDirPath + "/"
                + TEXT_MODEL_NAME);
        // 声学模型文件路径 (离线引擎使用)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, mSampleDirPath + "/"
                + SPEECH_FEMALE_MODEL_NAME);
        // 本地授权文件路径,如未设置将使用默认路径.设置临时授权文件路径,LICENCE_FILE_NAME请替换成临时授权文件的实际路径,仅在使用临时license文件时需要进行设置,如果在[应用管理]中开通了正式离线授权,不需要设置该参数,建议将该行代码删除(离线引擎)
        // 如果合成结果出现临时授权文件将要到期的提示,说明使用了临时授权文件,请删除临时授权即可。
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_LICENCE_FILE, mSampleDirPath + "/"
                + LICENSE_FILE_NAME);
        // 请替换为语音开发者平台上注册应用得到的App ID (离线授权)
        this.mSpeechSynthesizer.setAppId("xxx"/*这里只是为了让Demo运行使用的APPID,请替换成自己的id。*/);
        // 请替换为语音开发者平台注册应用得到的apikey和secretkey (在线授权)
        this.mSpeechSynthesizer.setApiKey("xxx",
                "xxx"/*这里只是为了让Demo正常运行使用APIKey,请替换成自己的APIKey*/);
        // 发音人(在线引擎),可用参数为0,1,2,3。。。(服务器端会动态增加,各值含义参考文档,以文档说明为准。0--普通女声,1--普通男声,2--特别男声,3--情感男声。。。)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0");
        // 设置Mix模式的合成策略
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_MIX_MODE, SpeechSynthesizer.MIX_MODE_DEFAULT);
        // 授权检测接口(只是通过AuthInfo进行检验授权是否成功。)
        // AuthInfo接口用于测试开发者是否成功申请了在线或者离线授权,如果测试授权成功了,可以删除AuthInfo部分的代码(该接口首次验证时比较耗时),不会影响正常使用(合成使用时SDK内部会自动验证授权)
        AuthInfo authInfo = this.mSpeechSynthesizer.auth(TtsMode.MIX);

        if (authInfo.isSuccess()) {
            Toast.makeText(this,"auth success",Toast.LENGTH_LONG).show();
        } else {
            String errorMsg = authInfo.getTtsError().getDetailMessage();
            Toast.makeText(this,"auth failed errorMsg=" + errorMsg,Toast.LENGTH_LONG).show();
        }

        // 初始化tts
        mSpeechSynthesizer.initTts(TtsMode.MIX);
        // 加载离线英文资源(提供离线英文合成功能)
        int result =
                mSpeechSynthesizer.loadEnglishModel(mSampleDirPath + "/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath
                        + "/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
        //Toast.makeText(this,"loadEnglishModel result=" + result,Toast.LENGTH_LONG).show();

        //打印引擎信息和model基本信息
        //printEngineInfo();
    }
    private void speak() {
        String text = this.mInput.getText().toString();
        //需要合成的文本text的长度不能超过1024个GBK字节。
        if (TextUtils.isEmpty(mInput.getText())) {
            text = "欢迎使用百度语音合成SDK,百度语音为你提供支持。";
            mInput.setText(text);
        }
        int result = this.mSpeechSynthesizer.speak(text);
        if (result < 0) {
            Toast.makeText(this,"error,please look up error code in doc or URL:http://yuyin.baidu.com/docs/tts/122 ",Toast.LENGTH_LONG).show();
        }
    }

    private void initialEnv() {
        if (mSampleDirPath == null) {
            String sdcardPath = Environment.getExternalStorageDirectory().toString();
            mSampleDirPath = sdcardPath + "/" + SAMPLE_DIR_NAME;
        }
        makeDir(mSampleDirPath);
        copyFromAssetsToSdcard(false, SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_FEMALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_MALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, TEXT_MODEL_NAME, mSampleDirPath + "/" + TEXT_MODEL_NAME);
        copyFromAssetsToSdcard(false, LICENSE_FILE_NAME, mSampleDirPath + "/" + LICENSE_FILE_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_SPEECH_MALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_TEXT_MODEL_NAME);
    }

    private void makeDir(String dirPath) {
        File file = new File(dirPath);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    /**
     * 将sample工程需要的资源文件拷贝到SD卡中使用(授权文件为临时授权文件,请注册正式授权)
     *
     * @param isCover 是否覆盖已存在的目标文件
     * @param source
     * @param dest
     */
    private void copyFromAssetsToSdcard(boolean isCover, String source, String dest) {
        File file = new File(dest);
        if (isCover || (!isCover && !file.exists())) {
            InputStream is = null;
            FileOutputStream fos = null;
            try {
                is = getResources().getAssets().open(source);
                String path = dest;
                fos = new FileOutputStream(path);
                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = is.read(buffer, 0, 1024)) >= 0) {
                    fos.write(buffer, 0, size);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    @Override
    protected void onStop() {
        super.onStop();
        // 停止唤醒监听
        mWpEventManager.send("wp.stop", null, null, 0, 0);
    }


}

注:源码是将官网给的demo进行整合,并且删除了一些用不到的方法,简少了代码量。

activity_main:只有一个TextView和一个EditView,很简单。TextView用于显示结果,EditView用于语音合成的文字信息

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.administrator.baiduvoicetest.MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18dp"
        android:padding="8dp"
        android:id="@+id/txtResult" />
    <EditText
        android:id="@+id/input"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:hint="input" />
</LinearLayout>

AndroidManifest:添加权限和一个作为语音识别的UI的活动

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.administrator.baiduvoicetest">

    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />



    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">


        <!-- begin: baidu speech sdk-->
        <!--
            离线功能指南:
            1. 在百度语音开放平台注册应用,http://yuyin.baidu.com/app
            2. 为您的应用的“申请离线授权页面”,填写程序包名
            3. 在当前应用的AndroidManifest.xml中填写相应的APP_ID(或在代码中设置appid参数)
            4. 根据场景下载并集成相应的资源,见http://yuyin.baidu.com/docs/asr/131和http://yuyin.baidu.com/asr/download

            另外需要注意的是离线功能只是在线功能的“增强”,不能永久不联网使用(特别是首次使用)。
        -->
        <!-- 请填写真实的APP_ID API_KEY SECRET_KEY -->
        <meta-data android:name="com.baidu.speech.APP_ID" android:value="8888274"/>
        <meta-data android:name="com.baidu.speech.API_KEY" android:value="FOFOGnjFERG3UTZC4FdDnXhM"/>
        <meta-data android:name="com.baidu.speech.SECRET_KEY" android:value="63830985f5b05d2863f13ad07c7feaa3"/>
        <service android:name="com.baidu.speech.VoiceRecognitionService" android:exported="false" />
        <activity
            android:name="com.baidu.voicerecognition.android.ui.BaiduASRDigitalDialog"
            android:configChanges="orientation|keyboardHidden|screenLayout"
            android:theme="@android:style/Theme.Dialog"
            android:exported="false"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="com.baidu.action.RECOGNIZE_SPEECH" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        <!-- end : baidu speech sdk-->


        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

完整Android Studio工程:

http://download.csdn.net/detail/amazinguu/9682847

注:程序在真机可以运行,在虚拟机里运行会报错。

—————————————————————————————————

2017.1.13 更新

上文中我遇到的集成问题应该是我测试的真机型号(i9300)的问题,真机换成荣耀平板2,原程序就跑不通了,必须要将语音识别和语音合成的assert和jniLibs文件都放进工程里才能跑通。所以说正常的集成步骤应该是将语音识别和合成的Jar包放进libs里,assert和jniLibs还有res也都放进工程中对应的位置。就是参考上文中给的资料就可以了。

http://blog.csdn.net/kjunchen/article/details/51093134

代码中有一个bug,开启唤醒是放在onResume()里的,而停止监听是放在onStop()里的,这样的话,每一次语音识别完成后都会开启一次监听,而上次的监听并没有停止。这样的话在唤醒次数较少时没有问题,但在唤醒次数较多时,开启的唤醒线程太多,影响性能,导致唤醒效果不好。所以应该将开启唤醒放在onStart()里,这样语音识别完成后不会再次开启唤醒监听,始终只有一个唤醒监听线程,改进唤醒效果。

另外在同一台设备上两个不同应用同时使用离线语音合成功能,会提示授权失败。即一个开发者账号同时开通两个应用的的语音合成功能(App ID不同),在线状态下,先打开应用①,再打开应用②,都能够授权成功,然后离线,先打开应用②会显示授权成功,接着再打开应用①会显示授权失败,最后再打开应用②,也会显示授权失败。如果先打开应用①,那么①和②都会显示授权失败。这个原因应该是在线状态下,会将当前应用的授权文件会拷贝到SD卡中,后打开的应用会代替之前应用的授权文件,然后离线,这时候打开的应用会从SD卡中读取授权文件,如果SD卡中的授权文件与该应用不一致,就会导致授权失败。即在线状态下,后打开的应用②会代替之前应用①的授权文件存在SD卡里,然后离线如果直接打开应用②,因为SD卡里存的就是应用②的授权文件,所以会显示授权成功,然后打开应用①,因为SD卡里存的是应用②的授权文件,会显示授权失败。至于为什么再打开应用②也会显示授权失败,目前还不清楚。

上面逻辑有点绕,但总体意思就是不要在同一台设备上两个不同应用同时使用离线语音合成功能。

源码地址:Android 基于百度语音的语音交互
如果觉得有帮助,麻烦点下star,谢谢

猜你喜欢

转载自blog.csdn.net/AmazingUU/article/details/53164553