做独立播放器,是自己想怎么读就怎么读;做系统 TTS 引擎,是别人(阅读 App)说了算,你只负责把活接住。这个区别决定了整个结构。
最小的骨架
核心是继承 TextToSpeechService,在 manifest 里声明服务,并让系统能在「文字转语音输出」设置里找到它:
<service android:name=".MimoTtsService"
android:label="@string/engine_name">
<intent-filter>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent-filter>
<meta-data android:name="android.speech.tts"
android:resource="@xml/tts_engine" />
</service>
xml/tts_engine 里指向一个 settings Activity,用来放语速、音色这些开关。系统设置页会直接拉起它。
三个必须实现的方法
onIsLanguageAvailable(lang, country, variant):别偷懒一律返回LANG_AVAILABLE,中文简繁、中英混排的判断要在这里说清楚,不然阅读 App 会误判可用性。onGetVoices()/onGetDefaultVoiceNameFor():较新版本靠这个列音色,返回空列表会让部分客户端直接禁用引擎。onSynthesizeText(request, callback, params):真正干活的地方,用SynthesisCallback依次start()→audioAvailable(byte[])→done()。
override fun onSynthesizeText(
request: SynthesisRequest,
callback: SynthesisCallback,
params: Bundle
) {
val rate = request.speechRate // 客户端 setSpeechRate() 传进来的
callback.start(SAMPLE_RATE, AudioFormat.ENCODING_PCM_16BIT, 1)
engine.stream(request.charSequenceText.toString(), rate) { chunk ->
if (!callback.isDone) callback.audioAvailable(chunk, 0, chunk.size)
}
callback.done()
}
几个容易踩的点:audioAvailable 要在合成线程里持续喂,别等整段合成完一次性给,长章节会明显感到起播慢;onStop() 必须真的把合成任务取消掉,翻页时系统会频繁调用;回调期间要检查 callback.isDone,否则中断后再写会抛异常。
Android 16 上要多留意的
targetSdk 提到新版本后,后台启动和前台服务的限制更严,长时间朗读别依赖一个常驻后台服务自己保活,跟着系统的合成生命周期走更稳。另外 SynthesisRequest 里能拿到的参数比想象中少,语速、音调之外不要指望客户端把自定义字段透传过来——真要扩展,走自己的 settings 加共享配置更现实。
调试时最省事的办法,是写一个只调 TextToSpeech 并显式指定引擎包名的小测试 App,比在系统设置里来回切引擎快得多。