Android TTS系列一如何让app具备tts能力

2020-12-23 15:14:23

参考地址 Android TTS系列一——如何让app具备tts能力

源码地址:https://github.com/yellowgreatsun/MXTtsEngine

自2016年阿尔法狗大胜李世石以来,AI迅速普及,而其中语言AI就是落地化比较彻底的应用,也自然成了许多应用、智能硬件的标配功能。本人目前主要从事Android开发,2018年曾对tts做了比较多的研究,这几天,就想把之前的笔记再好好整理一番,写成博客分享给更多的开发者。

Android TTS系列

  • 如何让app具备tts能力?

  • 如何开发一款系统级tts引擎?

  • Android speech包源码剖析

本篇就先从第一项说起。
对应着MXTtsEngine的bdtts包和speech包。

让app具备tts能力,有两种方式,一种是直接集成第三方tts sdk,比如科大讯飞、百度、思必驰、小爱等等,调用sdk的接口即可。第二种是直接调用Android speech包下的tts接口,但其前提是Android设备已经安装了tts引擎。

一、直接集成第三方tts sdk

这里,以集成百度tts sdk为例。其它几种sdk,集成方式类似。

申请账号信息
  1. 进入百度AI开发平台,从“产品服务”中选择“语言合成”,可以查看官方介绍、在线体验合成效果。

  2. 进入控制台,在“产品服务”中选择“百度语音”后,点击“创建应用”,然后根据提示一步步填写信息,直至创建完成。而后,就可以看到所创建应用到账号信息,包括AppID、API Key、Secret Key,后面会用到。

集成tts sdk

如何集成tts sdk,详细信息可以查看官方文档。本文会进行简要介绍,代码可参考bdtts包下的BaiduTtsActivity.java。

  1. 集成jar包、so及其它资源。其中assets包下为离线语音包。


    集成tts sdk资源包.png

  2. 初始化引擎。
    创建引擎实例、设置listener、设置所需要的参数(比如发音人、音量、音速、音调、音频流类型、是否压缩)。

  private void initEngine() {

        // 1. 获取实例
        mSpeechSynthesizer = SpeechSynthesizer.getInstance();
        mSpeechSynthesizer.setContext(this);
        // 2. 设置listener
        mSpeechSynthesizer.setSpeechSynthesizerListener(speechSynthesizerListener);
        // 3. 设置appId,appKey.secretKey
        mSpeechSynthesizer.setAppId(appId);
        mSpeechSynthesizer.setApiKey(appKey, secretKey);
        // 4. 支持离线的话,需要设置离线模型
        if (ttsMode.equals(TtsMode.MIX)) {
            // 检查离线授权文件是否下载成功,离线授权文件联网时SDK自动下载管理,有效期3年,3年后的最后一个月自动更新。
            if (!checkAuth()) {
                return;
            }
            // 文本模型文件路径 (离线引擎使用), 注意TEXT_FILENAME必须存在并且可读
            mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, mApp.getTextModeFile());
            // 声学模型文件路径 (离线引擎使用), 注意TEXT_FILENAME必须存在并且可读
            mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, mApp.getSpeechModeFile());
        }
        // 5. 以下setParam 参数选填。不填写则默认值生效
        // 设置在线发声音人: 0 普通女声(默认) 1 普通男声 2 特别男声 3 情感男声<度逍遥> 4 情感儿童声<度丫丫>
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "4");
        // 其它参数设置这里略去
        // 6. 初始化
        mSpeechSynthesizer.initTts(ttsMode);
    }
  1. 语音播放或仅语言合成
    语音播放调用接口mSpeechSynthesizer.speak(text)。

mSpeechSynthesizer.speak("青青子衿,悠悠我心。");

仅语音合成调用接口mSpeechSynthesizer.synthesize(text),在回调中获取语音流。

mSpeechSynthesizer.synthesize("青青子衿,悠悠我心。");

语音流合成的开始、过程、结束;语音播放的开始、过程、结束;出现错误均在listener中回调。

  SpeechSynthesizerListener speechSynthesizerListener = new SpeechSynthesizerListener() {
        @Override
        public void onSynthesizeStart(String s) {
            //合成开始
            if (isNeedSaveTTS) {
                String filename = TimeUtil.getTimeStampLocal() + ".pcm";
                ttsFile = new File(destDir, filename);
                try {
                    if (ttsFile.exists()) {
                        ttsFile.delete();
                    }
                    ttsFile.createNewFile();
                    FileOutputStream ttsFileOutputStream = new FileOutputStream(ttsFile);
                    ttsFileBufferedOutputStream = new BufferedOutputStream(ttsFileOutputStream);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        @Override
        public void onSynthesizeDataArrived(String s, byte[] data, int i) {
            // 合成过程中的数据回调接口
            if(isNeedSaveTTS){
                try {
                    ttsFileBufferedOutputStream.write(data);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        @Override
        public void onSynthesizeFinish(String s) {
            // 合成结束
            if (isNeedSaveTTS)
                close();
        }
        @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) {
            // 合成和播放过程中出错时的回调
            if (isNeedSaveTTS)
                close();
        }
    };
  1. 释放引擎。
    调用stop、release接口。

   @Override
    protected void onDestroy() {
        if (mSpeechSynthesizer != null) {
            mSpeechSynthesizer.stop();
            mSpeechSynthesizer.release();
            mSpeechSynthesizer = null;
        }
        super.onDestroy();
    }

二、调用Android speech包下的接口

先看一下speech包。

speech包.png


Android从1.6起就提供了speech包,但是,speech包仅仅提供tts接口,而实现tts的能力,则需要安装第三方tts引擎。Android原生系统直接集成了pico引擎(仅支持英文合成),国内厂商也会集成一些tts引擎,比如我的小米手机集成了“小爱语音引擎”(早期的miui版本集成的是基于百度tts sdk开发的“语音合成引擎”)。具体可以到手机的 设置—语言和输入法—文字转语言(TTS)输出 下看一下支持哪些引擎,及默认引擎是哪一个。
自己从哪可以获取到tts引擎呢?我发现很难在应用商店或者官网上搜到,我自己收集到了几个引擎,上传到了百度云中。链接:https://pan.baidu.com/s/1E2OSb_jGG5dwm9YIe8_0QQ  密码:t8d8。


介绍了背景信息,接下来就开始看一下如何利用speech包来进行语言合成。代码可参考speech包下的TestSpeechActivity.java。

  1. 创建TextToSpeech对象,创建时传入OnInitListener监听器监听创建是否成功。

  textToSpeech = new TextToSpeech(this, new TextToSpeech.OnInitListener() {
            @Override
            public void onInit(int status) {
                // status : TextToSpeech.SUCCESS=0 , TextToSpeech.ERROR=-1
                Log.i(TAG, "TextToSpeech onInit status = " + status);
            }
        });

备注:创建TextToSpeech对象也可以调用包含指定包名的引擎,不指定的话就用默认引擎。

  1. 设置TextToSpeech所使用语言、国家选项,通过返回值判断TTS是否支持该语言、国家选项。

   int result = textToSpeech.setLanguage(Locale.CHINESE);

如果tts引擎不支持该语言,该接口就会返回TextToSpeech.LANG_MISSING_DATA或者result == TextToSpeech.LANG_NOT_SUPPORTED。

  1. 调用speak或synthesizeToFile方法,前者是语音播报,后者是仅语音合成。

mTTS.speak("青青子衿,悠悠我心", TextToSpeech.QUEUE_FLUSH, null,id);mTTS.synthesizeToFile("青青子衿,悠悠我心", null, file, id);

其结果会走setOnUtteranceProgressListener回调。

 textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
     @Override
     public void onStart(String utteranceId) {
     }
     @Override
      public void onDone(String utteranceId) {
      }
     @Override
      public void onError(String utteranceId) {
      }});
  1. 关闭TTS,回收资源。

    @Override
    protected void onDestroy() {
        // 4.关闭TTS,回收资源
        if (textToSpeech != null) {
            textToSpeech.stop();
            textToSpeech.shutdown();
        }
        super.onDestroy();
    }

三、二者比较

第一种是需要集成tts SDK,适合于为单个应用提供tts能力。
第二种是需要集成tts引擎,适合于集成到手机或其它Android设备中,这样该设备的所有应用均可以直接利用Android系统tts接口使用tts能力。

下一篇文章会介绍,如何开发tts引擎,敬请期待。




  • 2019-12-23 23:38:59

    vue-apollo的多客户端的用法

    vue-apollo的多客户端的用法以及apollo.js的配置 关于如何安装和如何使用,这篇文章就先暂时不介绍了,如果不清楚就看我另一篇关于vue-apollo的用法 在做项目中,有时候后端的接口是按模块功能去划分的,那么请求的地址就会不同,关于vue-apollo的多客户端配置如下

  • 2019-12-27 08:40:55

    align-self和align-items的区别

    align-items在伸缩容器上使用它,伸缩容器内部所有的元素都一致地受制于align-items的值。 但是有些时候,我们希望伸缩容器内部某个元素在侧轴上的排列方式有所差异。此时就不能使用 align-items,因为align-items作用于整体。我们希望作用于部分。这就是align-self的发挥场地。

  • 2019-12-29 15:01:37

    修改laravel分页的样式

    首先获取到数据,paginate方法 能够自动判定当前页面正确的数量限制和偏移数。默认情况下,当前页数由HTTP 请求所带的 ?page 参数来决定。当然,该值由 Laravel 自动检测,并自动插入由分页器生成的链接。

  • 2019-12-29 15:05:57

    php 数组分页 array_slice()函数用法

    今天用到一个函数,非常好用,分享给大家 array_slice() -从数组中取出一段 也就是说用这个函数可以和sql语句一样实现分页,原理是将查询出的数组,取出从指定下标开始到指定长度的数组

  • 2019-12-30 10:17:21

    router-link传递参数,query

    在vue-router中,有两大对象被挂载到了实例this; $route(只读、具备信息的对象); $router(具备功能的函数) 查询字符串: 去哪里 ? <router-link :to="{name:'detail',query:{id:1}}"> xxx </router-link>

  • 2019-12-30 16:48:41

    vue provide/inject详解和用法

    父子组件交互方式多种,props、vuex、 、 emit、localStorage还有就是这个provide/inject了。它适合层级比较深的组件,比如子,子孙,子孙后代的组件有好几个用到父组件的某个属性,就可以用到这个provide/inject,它可以避免写大量繁琐的传值代码 我这里为什么要使用它? 我一个知识库详情父组件中包含了大量的子组件,每个子组件都需要父组件的知识库ID,这时候我不想写大量props,就用到provide/inject进行传值了