Android TTS系列二—如何开发一款系统级tts引擎?

2020-12-23 15:15:28

参考地址 如何开发一款系统级tts引擎?

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

上篇文章Android TTS系列一——如何让app具备tts能力分享了如何通过第三方tts sdk和Android speech包下的接口来拥有tts能力,这次分享下如何开发一款系统级tts引擎。代码可参考ttsengine包。

先来看下speech包,有一个TextToSpeechService,是一个Service,很自然就想到开发一款引擎和它有关系。

speech包.JPG


那么就打开TextToSpeechService.java,从它的描述就可以看到,我们的推测是正确的。


/**
 * Abstract base class for TTS engine implementations. The following methods
 * need to be implemented:
 * <ul>
 * <li>{@link #onIsLanguageAvailable}</li>
 * <li>{@link #onLoadLanguage}</li>
 * <li>{@link #onGetLanguage}</li>
 * <li>{@link #onSynthesizeText}</li>
 * <li>{@link #onStop}</li>
 * </ul>……
**/

这里的描述还是很详细的,就是从一个tts引擎的Service需要继承TextToSpeechService,并且重写那五个方法。五个方法是干嘛的?这里简单阐述下:

  • int onIsLanguageAvailable(String lang, String country, String variant)
    是否支持该语言。语言通过lang、country、variant这三个Locale的字段来表示,意思分别是语言、国家和地区,比如zh-CN表示大陆汉语。这个方法看着简单,但我在这里栽坑了好久,就是因为对语言编码标准(ISO 639-1、ISO 639-2)不熟悉。

  • String[] onGetLanguage()
    获取当前引擎所设置的语言信息,返回值格式为{lang,country,variant}。

  • int onLoadLanguage(String lang, String country, String variant)
    设置该语言,并返回是否是否支持该语言。

  • void onStop()
    停止tts播放或合成。

  • void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback)
    将指定的文字,合成为tts音频流。
    从描述中可以看到,前三个方法主要描述语言,最后一个onSynthesizeText才是最关键的。

好啦,了解了TextToSpeechService,接下来就来开发tts引擎吧。
当然就是先创建一个Service,继承TextToSpeechService。需要注意的是,在mainfest中注册时需要声明下intent-filter,获取设备所有tts引擎时会用到这一点。

<service
  android:name="com.ishare.ttsengine.MoxiangTtsService"
  android:label="@string/app_name">
  <intent-filter>
     <action android:name="android.intent.action.TTS_SERVICE" />
    <category android:name="android.intent.category.DEFAULT" />
   </intent-filter>
  <meta-data
    android:name="android.speech.tts"
    android:resource="@xml/tts_engine" /></service>

接下来就是实现五个方法了,onGetLanguage、onLoadLanguage和onStop比较简单,可以直接看我源码,这里只将onIsLanguageAvailable和onSynthesizeText拿出来。

  1. int onIsLanguageAvailable(String lang, String country, String variant)
    我设定的是只支持zh-CN。

  @Override
  protected int onIsLanguageAvailable(String lang, String country, String variant) {
        if ((Locale.SIMPLIFIED_CHINESE.getISO3Language().equals(lang)) || (Locale.US.getISO3Language().equals(lang))) {
            if ((Locale.SIMPLIFIED_CHINESE.getISO3Country().equals(country)) || (Locale.US.getISO3Country().equals(country)))
                return TextToSpeech.LANG_COUNTRY_AVAILABLE;
            return TextToSpeech.LANG_AVAILABLE;
        }
        return TextToSpeech.LANG_NOT_SUPPORTED;
    }

该方法会先后对lang和country做判断。
需要特别注意的是,lang、country、variant这三个参数是ISO 639-2标准,所以比较时要调用Locale的getISO3Language()和.getISO3Country(),我就是在这里踩的坑。起初用的是getLanguage(),一直出现问题。

  1. onSynthesizeText(SynthesisRequest request, SynthesisCallback callback)
    有两个参数,request包含要合成的文字、语言信息,callback就是合成过程中的回调。就这样,从request中拿到“材料”,然后通过callback回传过去。

    private SynthesisCallback mCallback;
    @Override
    protected synchronized void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) {
        this.mCallback = callback;
        // 判断是否支持该语言
        int load = onLoadLanguage(request.getLanguage(), request.getCountry(), request.getVariant());
        if (load == TextToSpeech.LANG_NOT_SUPPORTED) {
            this.mCallback.error();
            return;
        }
        // 回调“开始”
        this.mCallback.start(SAMPLING_RATE_HZ, AudioFormat.ENCODING_PCM_16BIT, 1);
        // 开始“合成”
        final String text = request.getCharSequenceText().toString();
        mSpeechSynthesizer.synthesize(text);
         // 开启一个循环,直到合成结束
        isSynthesizing = true;
        while (isSynthesizing) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

注意,这里我是调用百度的tts sdk进行合成的,即mSpeechSynthesizer.synthesize(text)。这个接口,只是合成语音,通过callback将合成的语音流回传过去,由TextToSpeechService来决定是直接播放还是存储为文件,这部分将在下篇文章阐述。
百度tts上篇已经介绍了,这里不再全部贴出,只列出比较关键的,即SpeechSynthesizerListener的几个回调。

   SpeechSynthesizerListener speechSynthesizerListener = new SpeechSynthesizerListener() {

        @Override
        public void onSynthesizeStart(String s) {
            //合成开始
        }

        @Override
        public void onSynthesizeDataArrived(String s, byte[] data, int i) {
            // 合成过程中的数据回调接口
            final int maxBufferSize = mCallback.getMaxBufferSize();
            int offset = 0;
            while (offset < data.length) {
                int bytesToWrite = Math.min(maxBufferSize, data.length - offset);
                mCallback.audioAvailable(data, offset, bytesToWrite);
                offset += bytesToWrite;
            }
        }

        @Override
        public void onSynthesizeFinish(String s) {
            // 合成结束
            isSynthesizing = false;
            if (mCallback!=null) {
                mCallback.done();
            }
        }

        @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) {
            // 合成和播放过程中出错时的回调
            isSynthesizing = false;
            if (mCallback != null)
                mCallback.error();
        }
    };

可以看出,在onSynthesizeDataArrived中将拿到的语音流回传过去,onSynthesizeFinish表示合成结束了,也就调用callback.done了。

到这里,我们的tts引擎已经开发完成了。将编译好的apk install到手机中,就可以通过speech包下的接口,来借助该引擎来具备tts能力了。
是不是很简单?

如果有小伙伴看了我源码,可能会好奇,为什么ttsengine包下还有其他几个java文件呢?它们是干嘛的呢?


ttsengine.jpg

来,打开你的手机,进入 设置—语言和输入法—文字转语言(TTS)输出,是不是看到了有设置、语言、收听示例?ttsengine包下的EngineSettings、CheckVoiceData和GetSampleText就是针对它们处理的,比较简单,就不阐述了。
小伙伴们可能想问,简单是简单,你怎么知道是这样写的?我,也是看了系统“设置”应用的源码知道的,遵循它的规则嘛。

OK,“如何开发一款系统级tts引擎?”就介绍到这里,下一篇我们就该一起看下speech包的源码了,一起探索TextToSpeech的调用接口如何与tts引擎关联起来的。



作者:抹香君
链接:https://www.jianshu.com/p/cbcc71add710
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


  • 2017-02-10 16:22:13

    git历史记录查询

    查看提交历史:git log 查看提交历史并显示版本间的差异:git log -p 查看指定历史:git log xxx(sha1值) -p 查看提交历史(指定时间):

  • 2017-02-13 17:50:05

    cURL error 60: SSL certificate problem: unable to get local issuer certificate

    Drupal 8 version uses Guzzle Http Client internally, but under the hood it may use cURL or PHP internals. If you installed PHP cURL on your PHP server it typically uses cURL and you may see an exception with error Peer certificate cannot be authenticated with known CA certificates or error code CURLE_SSL_CACERT (60).

  • 2017-02-16 08:09:01

    HTML中PRE和p的区别

    pre 元素可定义预格式化的文本。被包围在 pre 元素中的文本通常会保留空格和换行符。而文本也会呈现为等宽字体。 <pre> 标签的一个常见应用就是用来表示计算机的源代码。

  • 2017-02-16 15:14:14

    动态加载js和css

    开发过程中经常需要动态加载js和css,今天特意总结了一下常用的方法。