开发一个会叫自己“爷爷”的“孙子”,是一种什么样的体验?

Posted 宾有为

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开发一个会叫自己“爷爷”的“孙子”,是一种什么样的体验?相关的知识,希望对你有一定的参考价值。

独居的生活很是无聊,如果有什么成精的东西和我聊聊天就好了…

“独居的生活很是无聊,如果有什么成精的东西和我聊聊天就好了”,基于这个独特的想法,我,决定让某一样东西成精,赋予它阅读指定文字的能力。

目前市面上有两款产品可以较好的实现语音相关的功能,分别是百度语音识别与科大讯飞语音识别,在这两个中我选科大讯飞。如果Python、Node.js、C#、C++、php作为你的开发语言,百度语音识别可以找到相关文档。如果开发的语音识别是搭载着HarmonyOS系统上,可以选科大讯飞。二者各有所长、各有所短。

在操作前需要先前往官网下载语音相关的demo,demo里面有我们集成语音技术必要的资源。

下载后,将assets、libs文件夹拷贝至自己的项目,在androidManifest.xml静态声明部分权限。

<!--连接网络权限,用于执行云端语音能力 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!--获取手机录音机使用权限,听写、识别、语义理解需要用到此权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--读取手机信息权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!--外存储写权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外存储读权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--配置权限,用来记录应用配置信息 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<!--手机定位信息,用来为语义等功能提供定位,提供更精准的服务-->
<!--定位信息是敏感信息,可通过Setting.setLocationEnable(false)关闭定位请求 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

接着动态申请危险权限:

/**
* android 6.0 以上需要动态申请录制音频、写外部存储权限
*/
private void initPermission() {
   String permissions[] = {Manifest.permission.RECORD_AUDIO,
           Manifest.permission.WRITE_EXTERNAL_STORAGE
   };
   ArrayList<String> toApplyList = new ArrayList<String>();

   for (String perm : permissions) {
       if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(this,
               perm)) {
           toApplyList.add(perm);
       }
   }
   String tmpList[] = new String[toApplyList.size()];
   if (!toApplyList.isEmpty()) {
       ActivityCompat.requestPermissions(this, toApplyList.toArray(tmpList), 123);
   }
}

使用科大讯飞语音识别需要初始化,初始化即创建语音配置对象,只有初始化后才可以使用MSC的各项服务。建议将初始化放在程序入口处(如Application、Activity的onCreate方法)。

// 将“=”后面的字符串替换成您申请的APPID,申请地址:http://www.xfyun.cn
// 请勿在“=”与appid之间添加任何空字符或者转义符
SpeechUtility.createUtility(this, "appid=" + getString(R.string.app_id));

实现语音识别监听

/**
* 语音识别监听器
 */
private RecognizerDialogListener mRecognizerDialogListener = new RecognizerDialogListener() {
    public void onResult(RecognizerResult results, boolean isLast) {
        if (!isLast) {
            // 识别结果
        }
    }
    // 识别回调错误.
    public void onError(SpeechError error) {
        Toast.makeText(MainActivity.this, error.getPlainDescription(true),
                Toast.LENGTH_SHORT).show();
    }
};

接着就是 call 孙子啦

public void call() {
    // 使用SpeechRecognizer对象,可根据回调消息自定义界面;
    mIat = SpeechRecognizer.createRecognizer(this, mInitListener);
    if (null == mIat) {
        // 创建单例失败,与 21001 错误为同样原因,参考 http://bbs.xfyun.cn/forum.php?mod=viewthread&tid=9688
        Toast.makeText(this, "创建对象失败,请确认 libmsc.so 放置正确,且有调用 createUtility 进行初始化",
                Toast.LENGTH_SHORT).show();
        return;
    }
    // 清除数据
    mIatResults.clear();
    // 设置参数
    // 清空参数
    mIat.setParameter(SpeechConstant.PARAMS, null);
    // 设置听写引擎类型
    mIat.setParameter(SpeechConstant.ENGINE_TYPE, SpeechConstant.TYPE_CLOUD);
    // 设置返回结果的数据格式
    mIat.setParameter(SpeechConstant.RESULT_TYPE, "json");

    if (language.equals("zh_cn")) {
        String lag = mSharedPreferences.getString("iat_language_preference",
                "mandarin");
        // 设置语言
        mIat.setParameter(SpeechConstant.LANGUAGE, "zh_cn");
        // 设置语言区域
        mIat.setParameter(SpeechConstant.ACCENT, lag);
    } else {
        mIat.setParameter(SpeechConstant.LANGUAGE, language);
    }
    //此处用于设置dialog中不显示错误码信息
    mIat.setParameter("view_tips_plain", "false");

    // 设置语音前端点:静音超时时间,即用户多长时间不说话则当做超时处理
    mIat.setParameter(SpeechConstant.VAD_BOS, mSharedPreferences.getString(
            "iat_vadbos_preference", "4000"));

    // 设置语音后端点:后端点静音检测时间,即用户停止说话多长时间内即认为不再输入, 自动停止录音
    mIat.setParameter(SpeechConstant.VAD_EOS, mSharedPreferences.getString(
            "iat_vadeos_preference", "1000"));

    // 设置标点符号,设置为"0"返回结果无标点,设置为"1"返回结果有标点
    mIat.setParameter(SpeechConstant.ASR_PTT, mSharedPreferences.getString(
            "iat_punc_preference", "1"));

    // 设置音频保存路径,保存音频格式支持pcm、wav,设置路径为sd卡请注意WRITE_EXTERNAL_STORAGE权限
    mIat.setParameter(SpeechConstant.AUDIO_FORMAT, "wav");
    mIat.setParameter(SpeechConstant.ASR_AUDIO_PATH,
            Environment.getExternalStorageDirectory() + "/msc/iat.wav");
    mIatDialog.setListener(mRecognizerDialogListener);//设置监听
    mIatDialog.show();// 显示对话框
}

看到show()方法没?随着show()方法的执行,语音识别功能也就完成了喔


爷爷问话了,此时的孙子还不能回复爷爷的话,对于爷爷的招呼,也只能默默的看着爷爷,像个木头人似的不知所措。

孙子发言,需要使用语音合成。语音合成,与语音听写相反,语音合成是将一段文字转换为语音,可根据需要合成出不同音色、语速和语调的声音,让机器像人一样开口说话,不仅如此,还能根据个人需求,更换孙子或孙女,高端的孙子、孙女往往都可以识别民族语言、多国语言的喔。

孙子怎么知道应该回复什么信息呢?

回复的信息是我们提前预设好的,发言的不同,回复的内容也不一样。语音识别和命令词识别有一点区别,命令词识别是识别语音并提取提前预设的关键词通过文本输出。而语音识别则是直接将一串语音翻译成文本直接输出。

我把爷爷发言与孙子的答复提前插入Sqlite,待语音识别到的文本与存储在Sqlite里爷爷的发言一致时,取对应的答案,通过语音输出,孙子也就不再是一个哑巴了。

那么,我们来实现孙子的语音回复吧。

/**
* 孙子回复
* @param answer 孙子回复内容
 */
private void grandsonAnswer(String answer) {
    // 初始化云端发音人名称列表
    cloudVoicersEntries = getResources().getStringArray(R.array.voicer_cloud_entries);
    cloudVoicersValue = getResources().getStringArray(R.array.voicer_cloud_values);
    // 初始化合成对象
    mTts = SpeechSynthesizer.createSynthesizer(this, mInitListener);
    // 清空参数
    mTts.setParameter(SpeechConstant.PARAMS, null);
    //设置合成
    //设置使用云端引擎
    mTts.setParameter(SpeechConstant.ENGINE_TYPE, SpeechConstant.TYPE_CLOUD);
    //设置发音人
    mTts.setParameter(SpeechConstant.VOICE_NAME, voicerCloud);
    //mTts.setParameter(SpeechConstant.TTS_DATA_NOTIFY,"1");//支持实时音频流抛出,仅在synthesizeToUri条件下支持
    //设置合成语速
    mTts.setParameter(SpeechConstant.SPEED, "50");
    //设置合成音调
    mTts.setParameter(SpeechConstant.PITCH, "50");
    //设置合成音量
    mTts.setParameter(SpeechConstant.VOLUME, "50");
    //设置播放器音频流类型
    mTts.setParameter(SpeechConstant.STREAM_TYPE, "3");
    //	mTts.setParameter(SpeechConstant.STREAM_TYPE, AudioManager.STREAM_MUSIC+"");

    // 设置播放合成音频打断音乐播放,默认为true
    mTts.setParameter(SpeechConstant.KEY_REQUEST_FOCUS, "true");

    // 设置音频保存路径,保存音频格式支持pcm、wav,设置路径为sd卡请注意WRITE_EXTERNAL_STORAGE权限
    mTts.setParameter(SpeechConstant.AUDIO_FORMAT, "wav");

    mTts.setParameter(SpeechConstant.TTS_AUDIO_PATH,
            Environment.getExternalStorageDirectory() + "/msc/tts.wav");
    int code = mTts.startSpeaking(answer, mTtsListener);
    if (code != ErrorCode.SUCCESS) {
        Toast.makeText(this, "语音合成失败,错误码: " + code + ",请点击网址https://www.xfyun" +
                ".cn/document/error-code查询解决方案", Toast.LENGTH_SHORT).show();
    }
}

代码copy一下运行起来便是这样的结果了 ↓

如果想听孙子叫爷爷,那调用方法的时候直接传一个爷爷的参数就可以啦

public void callGrandpa(View view) {
    grandsonAnswer("爷爷~");
}


孙子的声音听腻了,还可以切换成孙女的声音

/**
* 切换孙子或孙女
*/
public void update(View view) {
   new AlertDialog.Builder(this).setTitle("更换孙子或孙女")
           .setSingleChoiceItems(cloudVoicersEntries, // 单选框有几项,各是什么名字
                   selectedNumCloud, // 默认的选项
                   new DialogInterface.OnClickListener() { // 点击单选框后的处理
                       public void onClick(DialogInterface dialog,
                                           int which) { // 点击了哪一项
                           voicerCloud = cloudVoicersValue[which];
                           selectedNumCloud = which;
                           dialog.dismiss();
                       }
                   }).show();
}

可切换的孙子、孙女

<!-- 合成 -->
 <string-array name="voicer_cloud_entries">
     <item>小燕</item>
     <item>小宇</item>
     <item>凯瑟琳</item>
     <item>亨利</item>
     <item>玛丽</item>
     <item>小研</item>
     <item>小琪</item>
     <item>小峰</item>
     <item>小梅</item>
     <item>小莉</item>
     <item>小蓉</item>
     <item>小芸</item>
     <item>小坤</item>
     <item>小强 </item>
     <item>小莹</item>
     <item>小新</item>
     <item>楠楠</item>
     <item>老孙</item>
 </string-array>
 <string-array name="voicer_cloud_values">
     <item>xiaoyan</item>
     <item>xiaoyu</item>
     <item>catherine</item>
     <item>henry</item>
     <item>vimary</item>
     <item>vixy</item>
     <item>xiaoqi</item>
     <item>vixf</item>
     <item>xiaomei</item>
     <item>xiaolin</item>
     <item>xiaorong</item>
     <item>xiaoqian</item>
     <item>xiaokun</item>
     <item>xiaoqiang</item>
     <item>vixying</item>
     <item>xiaoxin</item>
     <item>nannan</item>
     <item>vils</item>
 </string-array>

到这里,能想到的功能也就都完成了,附上demo运行GIF


最后也不要忘记当界面销毁后,对应的变量应该释放内存

@Override
protected void onDestroy() {
    super.onDestroy();
    if( null != mIat ){
        // 退出时释放连接
        mIat.cancel();
        mIat.destroy();
    }
    if( null != mTts ){
        mTts.stopSpeaking();
        // 退出时释放连接
        mTts.destroy();
    }
}

本文代码已上传至:开发一个会叫自己“爷爷”的“孙子”,是一种什么样的体验?


参考文献:
1、Android 科大讯飞语音识别(详细步骤+源码)
2、科大讯飞在线语音听写 Android SDK 文档
3、科大讯飞在线语音合成 Android SDK 文档

以上是关于开发一个会叫自己“爷爷”的“孙子”,是一种什么样的体验?的主要内容,如果未能解决你的问题,请参考以下文章

开发一个会叫自己“爷爷”的“孙子”,是一种什么样的体验?

开发一个会叫自己“爷爷”的“孙子”,是一种什么样的体验?

P1498 南蛮图腾

P1498 南蛮图腾

洛谷 1498 南蛮图腾

P1498 南蛮图腾