Android TTS系列二——如何開發一款系統級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拿出來。
-
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(),一直出現問題。
-
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引擎關聯起來的。