深入 Android TTS 语音合成全链路:从 API 桥接到端侧声码器
做车载语音助手时,遇到一个问题:TTS 首字延迟(Time To First Audio)比竞品高了 300ms。排查调用链后发现,瓶颈不在网络,也不在模型推理——引擎服务的绑定过程吃掉 100ms+,而且每次合成都重新走一遍 setup。
Android TTS 框架的设计决定了这个局面:上层统一 API,但引擎选择、绑定、音频合成全部跑在独立进程的 Service 里。不把这套桥接机制吃透,延迟很难压下去。
TextToSpeech 初始化的真实开销
new TextToSpeech(context, listener) 的构造过程做了三件事:
- 通过
TtsEngines查询系统已注册的引擎列表 - 根据用户默认选择或 locale 匹配规则确定目标引擎
bindService跨进程绑定引擎的TextToSpeechService
核心代码路径在 TextToSpeech.java 的 initTts() 方法中:
// TextToSpeech 内部初始化链路(简化)
private int initTts(Context ctx, OnInitListener listener) {
// 1. 获取引擎信息
mEnginesHelper = new TtsEngines(ctx);
String engine = mEnginesHelper.getDefaultEngine();
// 2. 构建 Service Intent
Intent intent = new Intent(TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE);
intent.setPackage(engine);
// 3. 跨进程绑定——这才是耗时的根源
boolean bound = ctx.bindService(intent, mServiceConnection,
Context.BIND_AUTO_CREATE);
// 4. 等待 onServiceConnected 回调后设置语言
// setLanguage() 会在连接成功后异步调用
return bound ? SUCCESS : ERROR;
}
两个需要注意的点:setLanguage 只在 onInit 回调后才真正生效,构造完立刻 speak 可能因语言未加载而失败;每次 new TextToSpeech 都是一次 bindService,低端设备上跨进程通信耗时 80-120ms。
引擎插件化:靠一个 intent-filter 撑起生态
第三方引擎如何被框架发现?靠 AndroidManifest 中的 Service 声明。系统预置的 Pico TTS 和第三方引擎(Google TTS、讯飞等)用的是同一套注册方式:
<!-- 第三方 TTS 引擎的 Service 声明 -->
<service
android:name=".MyTtsService"
android:exported="true"
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_info" />
</service>
TtsEngines.getEngines() 用 PackageManager.queryIntentServices 扫描所有声明了该 action 的 Service,然后解析 tts_engine_info.xml 获取引擎支持的语言列表、采样率、性别偏好等元数据。引擎优先级不由用户评分决定,而是系统设置中的「首选引擎」配置项,存于 Settings.Secure.TTS_DEFAULT_SYNTH。
<!-- res/xml/tts_engine_info.xml -->
<tts-engine xmlns:android="http://schemas.android.com/apk/res/android"
android:locale="zh-CN"
android:gender="female"
android:networkRequired="false"
android:sampleRateInHz="24000" />
这种设计的核心价值在于:框架完全不关心里面是规则合成还是神经网络推理,实现了 onSynthesizeText 回调、写入标准 PCM 音频,就能接入。
合成回调链:字符串到 PCM 的异步路径
speak(text, queueMode, params, utteranceId) 的调用链路比看上去复杂。框架不会直接调到引擎的 onSynthesizeText,中间隔了一层 Action 队列:
speak() → mProcessingQueue → runAction()
→ mServiceConnection.getService().speak()
→ 引擎进程: onSynthesizeText(text, params, utteranceId)
→ 引擎内部: 文本处理 → 音频生成 → writeAudio()
→ 框架进程: AudioTrack 播放
mProcessingQueue 是单线程顺序执行队列,QUEUE_FLUSH 和 QUEUE_ADD 的差异只在是否清空队列,不改变后续 Action 的执行方式。
我实际测下来,引擎 onSynthesizeText 从调用到 writeAudio 首次回调的间隔,占首字延迟的大头。Google TTS 引擎在高端设备上这个时间约 50-80ms,加上队列等待和跨进程开销,TTFA 轻松超过 200ms。
端侧神经网络声码器:合成质量逼近真人
传统参数合成(formant synthesis)和拼接合成(concatenative)的声音机械感明显。端侧神经网络 TTS 通常分两段:
- 声学模型(Acoustic Model):文本 → Mel 频谱。常用 Tacotron2 或 FastSpeech 架构,推理出 80 维 Mel 频谱帧序列
- 声码器(Vocoder):Mel 频谱 → 音频波形。WaveRNN、HiFi-GAN 等 GAN 方案在质量和推理速度上平衡得不错
端侧落地的核心矛盾是模型体积和推理延迟的平衡。我用 TensorFlow Lite 部署过一个 HiFi-GAN 声码器,原始模型 14MB,INT8 量化后压到 3.5MB,单帧推理时间从 12ms 降到 4ms:
// TFLite 声码器推理(简化)
class HifiGanVocoder(modelPath: String) {
private val interpreter = Interpreter(File(modelPath))
fun melToAudio(mel: FloatArray): ShortArray {
val output = Array(1) {
ShortArray(mel.size * HOP_LENGTH) // hop_length=256
}
interpreter.run(mel, output)
return output[0] // 16-bit PCM
}
}
量化代价是音质轻微下降,MOS 分从 4.2 降到 4.0。换回的是骁龙 865 上稳定 4ms/帧的推理速度,配合 256 的 hop length,实时因子(RTF)约 0.06——远低于 1.0 的实时线。
实时合成流水线:分块推理 + 环形缓冲
长文本合成是端侧模型的另一个坎。等全文处理完再生成音频,卡顿感会很明显。做法是分句合成 + 流式输送音频:
输入文本 → 分句器 → [句子1, 句子2, ..., 句子N]
↓
声学模型推理 → Mel 帧序列
↓
声码器推理 → PCM 片段
↓
AudioTrack.write() → 即时播放
实现上用两个线程和环形缓冲区衔接各阶段。前台线程负责模型推理和音频生成,后台线程(AudioTrack 的 WRITE_NON_BLOCKING 模式)消费缓冲区:
// 环形缓冲流式播放
class StreamingTtsPlayer(bufferSizeMs: Int = 200) {
private val ringBuffer = ShortArray(SAMPLE_RATE * bufferSizeMs / 1000)
fun onAudioReady(pcm: ShortArray) {
// 写入环形缓冲,会自动等待 AudioTrack 消费
ringBuffer.write(pcm)
}
// AudioTrack 回调线程自动读取并播放
private val track = AudioTrack.Builder()
.setBufferSizeInBytes(ringBuffer.size * 2)
.setTransferMode(AudioTrack.MODE_STREAM)
.build()
}
这里踩过一个坑:AudioTrack 的 getMinBufferSize 在 Android 10 以下返回偏大(约 8KB),导致播放启动延迟多出 50-80ms。最后直接硬编码 buffer size 为采样率的 1/10(100ms 缓冲),高通和 MTK 平台均稳定跑了。
几点实践取舍
引擎初始化复用:不要在每个 Activity 里 new TextToSpeech。用 Application 级单例持有实例,首帧延迟能省 80ms+。
静默帧预填充:收到第一段有效音频前,先往 AudioTrack 写 20-30ms 静默帧。这不是为了音频填充,而是给 AudioTrack 底层 mixer 线程一个启动预热时间,实测减少 15-20ms 播放抖动。
模型热加载:App 启动后可能立刻要用 TTS 的话,把模型加载放到 Application.onCreate 里。TFLite 的 Interpreter 首次构造时需要分配 Native 内存和编译算子图,耗时 200-500ms,等用户触发时再加载来不及。
引擎选择:我的选择是优先用系统默认引擎(通常是 Google TTS),自研端侧模型作为降级。Google TTS 的语音自然度经过大规模数据训练,自研模型在短文本场景下很难拉开差距,但维护成本高出几个数量级。