android语音合成tts(云知声,百度tts,科大讯飞)

介绍

目前语音合成这方面做得最好的就是科大讯飞了,合成速度快,准确度高,模型多。但问题也是相当明显,只有在线合成是免费的,离线则是一笔不小的开销,个人到是可以申请免费的几个机器使用。百度tts则算是专门为他的导航做的一套吧,性能相对也还可以,但是他只支持离在线混合模式,默认是在wifi情况下是使用在线模式,4g或无网络情况下使用离线模式。云知声tts则可以实现完全的离线合成模式,当然,相比于上面的两个,性能可能没那没完美,不过对于一般的应用来说,完全足够了,唯一的缺点就是模型实在是太少了。

在线合成和离线合成(合成速度)

这个应该很好理解,在线合成必须将数据传到第三方平台,调用他们的服务接口进行合成,这中间牵扯到网络状况,在网络良好的情况下,合成速度和离线模式没有太大的差别,但是有时候服务器也会来大姨妈,比如我之前使用的是百度tts(理论上,百度tts的wifi在线模式下,合成时间超过0.5秒后会自动使用离线模式,但是并没有)就遇到过有一整天都延迟在2秒左右,后来放弃了。再也不相信在线模式了,你无法保证你的网络一直都是畅通无阻的。

问题

在线模式虽然不是很稳定,但是也有他的优点,不需要吧模型和合成底层代码放在本地,离线合成虽然稳定快速,但是apk体积增加的有点夸张

云知声

云知声的解决办法:把语音合成模型放在服务器后端,你要使用的时候下载到本地。

1

注册云知声开发者,创建应用,下载离线语音合成sdk,里面要有东西就两个。
一个是libs

libs

还有一个是assets
assets

将libs里面的所有东西都拷贝到你的项目的对应的libs下(比如app目录下的libs)
在你的模块的 gradle的android括号下加上

sourceSets {
    main {
        jniLibs.srcDirs = ['libs']
    }
}

assets文件夹里面的文件是合成模型文件,有两种处理方式。
第一种:将assets拷贝到你的项目的assets下,然后访问assets文件流,将assets里面的文件复制到本地(多此一举)。
第二种:直接将assets里面的文件复制到你手机的/unisound/tts(把我解压后直接发送到手机),如图

image.png

2

权限

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<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_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />

3

新建你的BaseApplication, 在 AndroidManifest里面声明,然后在BaseApplication中初始化你的TTs,
TTSUtils.getInstance().init();

附录(云知声的ttsutils)

public class TTSUtils implements SpeechSynthesizerListener {
    private static final String TAG = "TTSUtils";
    private static volatile TTSUtils instance = null;
    private boolean isInitSuccess = false;
    private SpeechSynthesizer mTTSPlayer;
    public static final String SAMPLE_DIR = Environment.getExternalStorageDirectory().getAbsolutePath() + "/unisound/tts/";
    public static final String FRONTEND_MODEL = "frontend_model";
    public static final String BACKEND_MODEL = "backend_lzl";
    private static final String APPKEY = "wiouzjcsmxvqes7ibqqm5bcgqrzrvddsvtceiwa5";//这里换成你的key和secret
    private static final String SECRET = "3e20d7aff586ffe7dce62b302e7cc378";
    private TTSUtils() {
    }
    public static TTSUtils getInstance() {
        if (instance == null) {
            synchronized (TTSUtils.class) {
                if (instance == null) {
                    instance = new TTSUtils();
                }
            }
        }
        return instance;
    }
    public void init() {
        try {
            Context context = BaseApplication.getContext();
            //判断文件是否完整
            File _FrontendModelFile = new File(SAMPLE_DIR + FRONTEND_MODEL);
            File _BackendModelFile = new File(SAMPLE_DIR + BACKEND_MODEL);
            //校验文件的完整性
            String file1 = getFileMD5(_FrontendModelFile);
            String file2 = getFileMD5(_BackendModelFile);
//            if(!file1.equals("27ce3b75c2784353e33840c5e63b5f0c")||!file2.equals("cfcdd50077ee5a6c5d673b728a8d6f5")){
            if(!file1.equals("27ce3b75c2784353e33840c5e63b5f0c")||!file2.equals("57c9e96801d1173186407193e26a5ecf")){
                _FrontendModelFile.delete();
                _BackendModelFile.delete();
                Toast.makeText(context, "下载离线包后即可使用语音合成功能!", Toast.LENGTH_SHORT).show();
                return;
            }
            mTTSPlayer = new SpeechSynthesizer(context, APPKEY, SECRET);
            mTTSPlayer.setOption(SpeechConstants.TTS_SERVICE_MODE, SpeechConstants.TTS_SERVICE_MODE_LOCAL); // 设置本地合成
            File file = new File(SAMPLE_DIR);
            if (!file.exists()) {
                file.mkdirs();
            }
            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_FRONTEND_MODEL_PATH, SAMPLE_DIR + FRONTEND_MODEL);// 设置前端模型
            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_BACKEND_MODEL_PATH, SAMPLE_DIR + BACKEND_MODEL);// 设置后端模型
//            setOption(int key, java.lang.Object value)
//            设置合成相关参数
//            示例:
//            设置合成语速 SpeechConstants.TTS_KEY_VOICE_SPEED 范围 0 ~ 100 int
//            设置合成音高 SpeechConstants.TTS_KEY_VOICE_PITCH 范围 0 ~ 100 int
//            设置合成音量 SpeechConstants.TTS_KEY_VOICE_VOLUME 范围 0 ~ 100 int
//            设置是否将英文按拼音读 SpeechConstants.TTS_KEY_IS_READ_ENLISH_IN_PINYIN 如:wang->王 boolean
//            设置语音结尾段的静音时长 SpeechConstants.TTS_KEY_BACK_SILENCE 0 ~ 1000 单位ms int
//            设置语音开始段的静音时长 SpeechConstants.TTS_KEY_FRONT_SILENCE 0 ~ 1000 单位ms int
            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_SPEED,85);
            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_PITCH,55);
//            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_VOLUME,200);
            //文字加数字,“玩啊”+106  106会读一百零六    “H”+106   会一个一个读出来 H 1 0 6
//            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_IS_READ_ENLISH_IN_PINYIN,false);
            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_BACK_SILENCE,300);//设置尾音
//            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_FRONT_SILENCE,1000);//设置开始音
            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_PLAY_START_BUFFER_TIME,250);//语音开始缓冲时间
//            mTTSPlayer.setOption(SpeechConstants.TTS_KEY_STREAM_TYPE, AudioManager.STREAM_SYSTEM);
            mTTSPlayer.setTTSListener(this);// 设置回调监听
            mTTSPlayer.init(null);// 初始化合成引擎
        } catch (Exception e) {
//            e.printStackTrace();
            Toast.makeText(BaseApplication.getContext(), "下载离线包后即可使用语音合成功能!", Toast.LENGTH_SHORT).show();
            return;
        }
    }
    /**获取文件的md5码,判断文件的完整性*/
    private static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException {
        if (!file.exists()||!file.isFile()) {
//            不是文件,或者不存在
            return "";
        }
        MessageDigest digest;
        FileInputStream in;
        byte buffer[] = new byte[1024];
        int len;
        digest = MessageDigest.getInstance("MD5");
        in = new FileInputStream(file);
        while ((len = in.read(buffer, 0, 1024)) != -1) {
            digest.update(buffer, 0, len);
        }
        in.close();
        BigInteger bigInt = new BigInteger(1, digest.digest());
        return bigInt.toString(16);
    }
    /**一个字节一个字节读*/
    public static void speckText(String msg){
        TTSUtils.getInstance().speak(getS(msg));
    }
    /**语义识别朗读*/
    public static void speeckTrueText(String msg){
        if(msg==null||"".equals(msg)){
            msg="";
        }
        if(msg.length()>25){
            msg=msg.substring(0,15);
        }
        TTSUtils.getInstance().speak(msg);
    }
    public static String getS(String msg){
        if(msg==null||"".equals(msg)){
            return "";
        }
        char[] chars = msg.toCharArray();
        StringBuilder sb=new StringBuilder();
        for (char aChar : chars) {
            sb.append(aChar+"\"");
        }
        return sb.toString();
    }
    public void speak(String msg) {
        try {
            if (isInitSuccess) {
                mTTSPlayer.playText(msg);
            }else {
                init();
            }
        } catch (Exception e) {
            Toast.makeText(BaseApplication.getContext(), "下载离线包后即可使用语音合成功能!", Toast.LENGTH_SHORT).show();
        }
    }
    public void stop() {
        mTTSPlayer.stop();
    }
    public void pause() {
        mTTSPlayer.pause();
    }
    public void resume() {
        mTTSPlayer.resume();
    }
    public void release() {
        if (null != mTTSPlayer) {
            // 释放离线引擎
            mTTSPlayer.release(SpeechConstants.TTS_RELEASE_ENGINE, null);
        }
    }
    @Override
    public void onEvent(int type) {
        switch (type) {
            case SpeechConstants.TTS_EVENT_INIT:
                isInitSuccess = true;
                break;
            case SpeechConstants.TTS_EVENT_SYNTHESIZER_START:
                // 开始合成回调
                Log.i(TAG, "beginSynthesizer");
                break;
            case SpeechConstants.TTS_EVENT_SYNTHESIZER_END:
                // 合成结束回调
                Log.i(TAG, "endSynthesizer");
                break;
            case SpeechConstants.TTS_EVENT_BUFFER_BEGIN:
                // 开始缓存回调
                Log.i(TAG, "beginBuffer");
                break;
            case SpeechConstants.TTS_EVENT_BUFFER_READY:
                // 缓存完毕回调
                Log.i(TAG, "bufferReady");
                break;
            case SpeechConstants.TTS_EVENT_PLAYING_START:
                // 开始播放回调
                Log.i(TAG, "onPlayBegin");
                break;
            case SpeechConstants.TTS_EVENT_PLAYING_END:
                // 播放完成回调
                Log.i(TAG, "onPlayEnd");
                break;
            case SpeechConstants.TTS_EVENT_PAUSE:
                // 暂停回调
                Log.i(TAG, "pause");
                break;
            case SpeechConstants.TTS_EVENT_RESUME:
                // 恢复回调
                Log.i(TAG, "resume");
                break;
            case SpeechConstants.TTS_EVENT_STOP:
                // 停止回调
                Log.i(TAG, "stop");
                break;
            case SpeechConstants.TTS_EVENT_RELEASE:
                // 释放资源回调
                Log.i(TAG, "release");
                break;
            default:
                break;
        }
    }
    @Override
    public void onError(int type, String errorMSG) {
        Log.e(TAG, "语音合成错误回调: " + errorMSG);
    }
    /**如果将assets里面的文件放在你自己的assets下,就需要用到下面的方法*/
//    public static void copyAssetsFile2SDCard(Context context, String fileName, String path) {
//        InputStream is=null;
//        FileOutputStream fos=null;
//        try {
//            is= context.getAssets().open(fileName);
//            fos= new FileOutputStream(new File(path));
//            byte[] buffer = new byte[1024];
//            int byteCount = 0;
//            while ((byteCount = is.read(buffer)) != -1) {// 循环从输入流读取buffer字节
//                fos.write(buffer, 0, byteCount);// 将读取的输入流写入到输出流
//            }
//           fos.flush();
//        } catch (IOException e) {
//            Log.e(TAG, "copyAssetsFile2SDCard: " + e.toString());
//        } finally {
//            if(fos!=null){
//                try {
//                    fos.close();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }
//            if(is!=null){
//                try {
//                    is.close();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
//    }
}

4调用

TTSUtils.speeckTrueText(msg);
或者TTSUtils.speckText(msg);

后记

在集成的时候遇到过很多bug,比如模型文件放在不正确的地方会导致没有声音,模型文件不完整的时候回导致程序崩溃,发音不是预期的效果等等,还有一些参数的设置,具体参数还是得看官方的开发文档,看api就行了。demo暂时没有整理比较乱,有需要的可以留言。

关于科大讯飞

科大讯飞也有一种离线的玩法,但是得下载一个语记app,然后在手机语言输入法设置下tts服务为科大讯飞的,然后调用android自带的语音合成,就是中文了(听说这里也可以调用讯飞在线的api进行设置也可以,本人没试过)。

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

推荐阅读更多精彩内容