Android集成百度TTS,实现离在线的中英语音合成

百度的离在线TTS,没有调用量限制,免费但是有QPS限制(是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准),增大QPS需要收费,所以对商用并不是很好友。如果想用完全免费的纯离线可参考我另一篇博客:
https://blog.csdn.net/sunyFS/article/details/97936551
话不多说开始!
第一步:先从百度tts官网下载离在线融合SDK,网址:https://ai.baidu.com/sdk#tts,解压后最好先运行一下demo。
参考技术文档:https://ai.baidu.com/docs#/TTS-Android-SDK/top
1.将com.baidu.tts_2.3.2.20180713_6101c2a.jar添加到你项目的libs(注意要添加依赖同步)
implementation files(‘libs/com.baidu.tts_2.3.2.20180713_6101c2a.jar’)
2.将assert文件下dat文件复制到你项目的assets下(没有该文件夹就创建)
// assets目录下bd_etts_text.dat为文本模型文件,
// assets目录下bd_etts_common_speech_m15_mand_eng_high_am-mix_v3.0.0_20170505.dat为离线男声模型;
// assets目录下bd_etts_common_speech_f7_mand_eng_high_am-mix_v3.0.0_20170512.dat为离线女声模型;
3.将jniLibs文件夹下的文件复制到你项目的jniLibs下
最终的目录结构为:Android集成百度TTS,实现离在线的中英语音合成_第1张图片

第二步:进入百度的控制台,创建语音合成的应用,包名可在配置清单文件的package查看
Android集成百度TTS,实现离在线的中英语音合成_第2张图片
获得对应的APPID,API KEY,Secret Key,包名,后面需要用到。
百度key
前期准备工作已经做好了,开始写代码!
按照文档在工程app目录下的proguard-rules.pro(混淆规则)文件里最后添加一下代码:

-keep class com.baidu.tts.**{*;}
-keep class com.baidu.speechsynthesizer.**{*;}

在配置清单文件中设置权限

   
    
    
    
    
    
    
    
android6.0需要进行动态权限的申请,需要将离线资源文件下载到本地,需要sd读写的权限,代码如下:
    private void initPermission() {
        String[] permissions = {
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
        };
        ArrayList mPermissionList = new ArrayList();
        mPermissionList.clear();
        for (int i = 0; i < permissions.length; i++) {
            if (ContextCompat.checkSelfPermission(this, permissions[i]) !=
                    PackageManager.PERMISSION_GRANTED) {
                mPermissionList.add(permissions[i]);//添加还未授予的权限到mPermissionList中
            }
        }
        //申请权限
        if (mPermissionList.size() > 0) {
            ActivityCompat.requestPermissions(this, permissions, 100);
        } else {
            //权限都已通过,进行初始化
            isFirstRun();

        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        boolean hasPermissionDismiss = false;//权限是否都已通过的标记
        if (requestCode == 100) {
            for (int i = 0; i < grantResults.length; i++) {
                if (grantResults[i] == -1) {
                    hasPermissionDismiss = true;
                    break;
                }
            }
        }
        if (hasPermissionDismiss) {//有未被允许的权限
            showPermissionDialog();
        } else {
            //初始化
            isFirstRun();

        }
    }


    /**
     * 手动设置权限
     */
    private void showPermissionDialog() {
        if (mPermissionDialog == null) {
            mPermissionDialog = new AlertDialog.Builder(this)
                    .setMessage("已禁用权限,请手动授予")
                    .setPositiveButton("设置", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            mPermissionDialog.cancel();
                            Uri packageURI = Uri.parse("package:" + mPackName);
                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
                            startActivity(intent);//打开应用设置
                            MainActivity.this.finish();
                        }
                    })
                    .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            mPermissionDialog.cancel();
                            MainActivity.this.finish();
                        }
                    })
                    .create();
        }
        mPermissionDialog.show();
    }

百度离在线模式的离线功能首次需要联网下载正式授权文件才可使用,所以进行首次启动app进行判断是否联网,使用sp保存首次启动的标记,用网络工具类进行判断是否有网,有网则初始化tts,无网则开辟线程进行循环判断(耗时操作,使用线程防止ANR),代码如下:

    private void isFirstRun() {
        SharedPreferences sp = getSharedPreferences("ttsFlag", MODE_PRIVATE);
        boolean firstFlag = sp.getBoolean("firstFlag", true);
        final SharedPreferences.Editor edit = sp.edit();
        Log.i("msg", "isFirstRun  firstFlag: " + firstFlag);
        if (firstFlag) {//第一次启动app,判断是否联网
            final int netFlag = NetUtil.getNetWorkState(MainActivity.this);
            Log.i("msg", "isFirstRun  netFlag: " + netFlag);
            if (netFlag == 0 || netFlag == 2) {//移动或者无线网络
                edit.putBoolean("firstFlag", false);
                edit.apply();

                initialEnv();
                initTts();
                initView();
            } else {//没有网络,
                Toast.makeText(this, "使用离线合成功能,首次联网!", Toast.LENGTH_SHORT).show();
                new Thread() {
                    @Override
                    public void run() {
                        int netFlag1 = NetUtil.getNetWorkState(MainActivity.this);
                        while (netFlag1 == 1) {
                            netFlag1 = NetUtil.getNetWorkState(MainActivity.this);
                        }
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                edit.putBoolean("firstFlag", false);
                                edit.apply();
                                initialEnv();
                                initTts();
                                initView();

                            }
                        });
                    }
                }.start();

            }
        } else {//非第一次启动app
            initialEnv();
            initTts();
            initView();
        }

    }

网络工具类代码如下:

public class NetUtil {
    //没有网络
    private static final int NETWORK_NONE = 1;
    //移动网络
    private static final int NETWORK_MOBILE = 0;
    //无线网络
    private static final int NETWORK_WIFI = 2;

    //获取网络启动
    public static int getNetWorkState(Context context) {
        ConnectivityManager connectivityManager = (ConnectivityManager) context
                //连接服务 CONNECTIVITY_SERVICE
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        //网络信息 NetworkInfo
        NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();

        if (activeNetworkInfo != null && activeNetworkInfo.isConnected()) {
            //判断是否是wifi
            if (activeNetworkInfo.getType() == (ConnectivityManager.TYPE_WIFI)) {
                //返回无线网络
//                Toast.makeText(context, "当前处于无线网络", Toast.LENGTH_SHORT).show();
                return NETWORK_WIFI;
                //判断是否移动网络
            } else if (activeNetworkInfo.getType() == (ConnectivityManager.TYPE_MOBILE)) {
//                Toast.makeText(context, "当前处于移动网络", Toast.LENGTH_SHORT).show();
                //返回移动网络
                return NETWORK_MOBILE;
            }
        } else {
            //没有网络
//            Toast.makeText(context, "当前没有网络", Toast.LENGTH_SHORT).show();
            return NETWORK_NONE;
        }
        //默认返回  没有网络
        return NETWORK_NONE;
    }

}

tts初始化,设置参数,离线资源路径等,记得替换成自己的apiid,apiKey, secretKey代码如下:

 private void initTts() {
        //获取实例
        mSpeechSynthesizer = SpeechSynthesizer.getInstance();
        mSpeechSynthesizer.setContext(this);
        mSpeechSynthesizer.setAppId(apiId);
        mSpeechSynthesizer.setApiKey(apiKey, secretKey);


        //文本模型文件路径 (离线引擎使用)
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, mSampleDirPath + "/"
                + ENGLISH_TEXT_MODEL_NAME);
        //声学模型文件路径 (离线引擎使用)
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, mSampleDirPath + "/"
                + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
        Log.i("msg", "initTts param: " + mSampleDirPath + "/" + ENGLISH_TEXT_MODEL_NAME);
        Log.i("msg", "initTts param: " + mSampleDirPath + "/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME);

        //模式:离在线混合
        mSpeechSynthesizer.auth(TtsMode.MIX);
        //对语音合成进行监听
        mSpeechSynthesizer.setSpeechSynthesizerListener(new listener());

        //设置参数
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0");//标准女声
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_VOLUME, "5");//音量 范围["0" - "15"], 不支持小数。 "0" 最轻,"15" 最响。
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, "5");//语速 范围["0" - "15"], 不支持小数。 "0" 最慢,"15" 最快
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_PITCH, "5");//语调 范围["0" - "15"], 不支持小数。 "0" 最慢,"15" 最快
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_MIX_MODE, SpeechSynthesizer.MIX_MODE_HIGH_SPEED_NETWORK);//WIFI,4G,3G 使用在线合成,其他使用离线合成 6s超时
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, mSampleDirPath + "/" + ENGLISH_TEXT_MODEL_NAME);//文本模型文件路径
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, mSampleDirPath + "/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME);//声学模型文件路径
        mSpeechSynthesizer.initTts(TtsMode.MIX);
    }

将app的离线资源文件复制到本地,在首次运行下载到本地后,后续进行文件存在判断,存在则不用在下载,代码如下:

    private void initialEnv() {
        if (mSampleDirPath == null) {
            String sdcardPath = Environment.getExternalStorageDirectory().toString();
            mSampleDirPath = sdcardPath + "/" + SAMPLE_DIR_NAME;
            Log.i("msg", "initialEnv mSampleDirPath: " + mSampleDirPath);// /storage/emulated/0/baiduTTS
        }
        File file = new File(mSampleDirPath);
        if (!file.exists()) {
            file.mkdirs();
        }

        copyFromAssetsToSdcard(false, ENGLISH_SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, ENGLISH_TEXT_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_TEXT_MODEL_NAME);


    }

/**
     * 将离线资源文件拷贝到SD卡中
     *
     * @param isCover 是否覆盖已存在的目标文件
     * @param source  dat文件
     * @param dest    保存文件路径
     */
    public 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();
                }
            }
        }
    }

tts监听类

public class listener implements SpeechSynthesizerListener {
    @Override
    public void onSynthesizeStart(String s) {
        Log.i("msg", "合成开始");
    }

    @Override
    public void onSynthesizeDataArrived(String s, byte[] bytes, int i) {
        Log.i("msg", "合成进度 :"+i);
    }

    @Override
    public void onSynthesizeFinish(String s) {

        Log.i("msg", "合成结束");
    }

    @Override
    public void onSpeechStart(String s) {
        Log.i("msg", "开始播放");
    }

    @Override
    public void onSpeechProgressChanged(String s, int i) {
        Log.i("msg", "播放进度 :"+i);
    }

    @Override
    public void onSpeechFinish(String s) {
        Log.i("msg", "合成结束");
    }

    @Override
    public void onError(String s, SpeechError speechError) {
        Log.i("msg", "error :"+speechError);
    }

使用相关方法进行播放,暂停,恢复播放,代码如下:

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt_start:
                Log.i("msg", "onClick text: " + et_input.getText().toString());
                mSpeechSynthesizer.speak(et_input.getText().toString());
                break;
            case R.id.bt_pause:
                mSpeechSynthesizer.pause();
                break;
            case R.id.bt_resume:
                mSpeechSynthesizer.resume();
                break;
            default:
                break;
        }
    }```


最后要记得释放资源

@Override
protected void onDestroy() {
    //释放tts资源
    if (mSpeechSynthesizer != null) {
        mSpeechSynthesizer.stop();
        mSpeechSynthesizer.release();
        mSpeechSynthesizer = null;
    }
    super.onDestroy();
}

该demo有几处缺陷;1.离线合成功能需要首次联网下载正式授权文件才可使用(官方sdk必须,除非你买纯离线)
2.在有网打开demo,合成引擎需要1s才初始化成功,无网络则大概3s才初始化成功(官方demo也是一样情况)。
3.调用量无限制,但是有QPS有限制(可以花钱扩大)。
关于正式授权文件,由于工作需要,想获取正式文件的路径,于是去问了下相关社区,回答是不提供的,想要的需要合作咨询(要钱!)。

demo的github:https://github.com/sunfusong/baiduTtsDemo
如果想了解纯离线、免费TTS(android原生TTS+语言引擎)也可以看下我的另一篇博客https://blog.csdn.net/sunyFS/article/details/97936551

项目在这 github:https://github.com/sunfusong/NativeTTS

写demo遇到的error:
1.org.apache.http.legacy.jar 找不到   
原因:android 9.0变更
解决方法:在配置清单文件下的

2.xml布局无法显示:Failed to load AppCompat ActionBar with unknown error.
原因:AS版本3.1发现的变化
解决方法:在style中修改