4. 语音合成与播报故障:TTS无声、音色异常、播报中断问题定位

各位同事,今天我们来聊聊TTS故障。说实话,语音合成这块儿,是我在座舱系统里最头疼的模块之一。它不像音频播放那么简单——播放个MP3,解码器坏了就是坏了。TTS不一样,它涉及文本分析、韵律生成、波形合成,任何一个环节出问题,结果就是要么不出声,要么声音怪怪的。

我遇到过最离谱的一次,是某款车型的导航语音突然变成了「机器人腔」,用户投诉说像在跟外星人对话。排查了两天,最后发现是语音库的采样率配置被OTA升级给覆盖了。嗯,这种坑踩过一次,你就再也不敢忽视配置管理了。

4.1 TTS无声故障:最让人抓狂的「哑巴」问题

TTS无声,说白了就是系统该说话的时候不说话。你按了导航键,屏幕在动,但喇叭里一片寂静。这种问题定位起来,其实有套路可循。

第一步:确认TTS引擎是否启动

我个人习惯,先看进程。在Linux座舱系统里,TTS通常是一个独立的后台服务。用ps | grep tts看一眼,如果进程都没了,那后面就不用查了。

# 检查TTS引擎进程
adb shell ps | grep -i tts
# 正常输出示例:
# root  1234  com.voice.ttsengine   ...   Running

# 如果没输出,说明引擎挂了
# 尝试手动拉起
adb shell am startservice com.voice.ttsengine/.TTSService

第二步:检查音频路由

引擎在跑,但没声音?那问题可能在音频通道上。TTS的音频流通常走的是「媒体通道」或「导航通道」,跟电话通道是分开的。我建议你用tinymix工具看看混音器的状态。

# 查看当前音频路由状态
adb shell tinymix | grep -i "TTS\|Voice"
# 检查TTS对应的Mixer是否处于Mute状态
# 如果发现类似 "TTS Volume" = 0 的情况,那就是被静音了
注意:有些座舱系统在静音按键按下时,会同时静音TTS通道。我曾经排查过一个案例,用户说导航没声音,结果发现是方向盘上的静音键卡住了。硬件问题,软件查了半天。

第三步:检查TTS回调状态

TTS引擎在合成完语音后,会通过回调接口通知应用层「我准备好了,可以播了」。如果回调没触发,或者回调里没调用播放接口,那引擎就是白干活。我建议你在TTS回调函数里加日志,看看onSpeechCompletedonError有没有被调用。

// 伪代码示例:TTS回调日志
ttsEngine.setOnUtteranceProgressListener(new UtteranceProgressListener() {
    @Override
    public void onStart(String utteranceId) {
        Log.d("TTS_DEBUG", "开始合成: " + utteranceId);
    }

    @Override
    public void onDone(String utteranceId) {
        Log.d("TTS_DEBUG", "合成完成: " + utteranceId);
        // 这里要检查是否调用了 AudioTrack.play()
    }

    @Override
    public void onError(String utteranceId) {
        Log.e("TTS_DEBUG", "合成出错: " + utteranceId);
    }
});

4.2 音色异常:从「播音员」到「机器人」的蜕变

音色异常,用户感知最明显。正常TTS听起来像真人,异常TTS听起来像早期的电子合成音。为什么会这样?我总结了几种常见原因。

异常现象 可能原因 排查方向
声音变尖、变细 采样率不匹配 检查TTS引擎输出采样率与音频设备支持的采样率是否一致
声音沙哑、有杂音 语音库损坏或格式错误 重新下载或校验语音包MD5
语速忽快忽慢 韵律参数被篡改 检查TTS引擎的语速、音调参数配置
特定文字发音错误 多音字词典缺失 更新TTS的多音字词库

采样率不匹配的坑

我遇到过最典型的案例:TTS引擎输出是16kHz,但音频设备配置成了48kHz。结果就是声音变尖,像快进播放一样。你想想看,16kHz的数据被当成48kHz来播放,相当于每个样本被拉伸了3倍,音调自然就高了。

// 检查TTS输出配置
TtsEngine.EngineConfig config = ttsEngine.getEngineConfig();
int sampleRate = config.getSampleRate(); // 期望是16000或22050

// 检查AudioTrack配置
AudioTrack track = new AudioTrack(
    AudioManager.STREAM_MUSIC,
    sampleRate,           // 这里必须与TTS引擎输出一致
    AudioFormat.CHANNEL_OUT_MONO,
    AudioFormat.ENCODING_PCM_16BIT,
    bufferSize,
    AudioTrack.MODE_STREAM
);
我的经验:遇到音色异常,先别急着怀疑语音库。先拿一段正常的PCM数据,用ffplay直接播放,看看是不是正常的。如果正常,说明问题在TTS引擎到播放器之间的参数传递上。如果不正常,那语音库或引擎本身有问题。

4.3 播报中断:说到一半就「卡壳」

播报中断,就是TTS说着说着突然停了,或者一句话被截成两段。这种问题在导航场景里特别烦人——「前方500米,右...」(然后没下文了)。用户不骂你才怪。

原因一:缓冲区溢出或欠载

TTS合成是实时的,引擎一边合成,播放器一边播。如果合成速度跟不上播放速度,缓冲区就会欠载,播放器没数据可播,自然就中断了。反过来,如果合成太快,缓冲区满了,引擎可能会被阻塞或丢弃数据。

// 检查缓冲区状态
// 在TTS回调中记录合成与播放的进度差
long synthesizedBytes = ttsEngine.getSynthesizedBytes();
long playedBytes = audioTrack.getPlaybackHeadPosition() * 2; // 16bit, mono
long bufferLevel = synthesizedBytes - playedBytes;

Log.d("TTS_BUF", "缓冲区级别: " + bufferLevel + " bytes");
// 如果 bufferLevel 持续接近0,说明欠载
// 如果 bufferLevel 持续增长,说明溢出

原因二:高优先级音频抢占

座舱系统里音频通道是有优先级的。电话 > 导航 > 媒体。如果TTS播到一半,突然来了个电话,系统可能会强制暂停TTS。但有时候电话挂断了,TTS却没有恢复播放。这就是典型的「中断后不恢复」问题。

我建议你在TTS播放器里监听音频焦点变化:

// 监听音频焦点变化
AudioManager.OnAudioFocusChangeListener focusListener = new AudioManager.OnAudioFocusChangeListener() {
    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_LOSS:
                // 永久失去焦点,暂停TTS
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                // 暂时失去焦点,暂停TTS
                break;
            case AudioManager.AUDIOFOCUS_GAIN:
                // 重新获得焦点,恢复TTS播放
                // 这里要检查是否需要从断点续播
                break;
        }
    }
};

原因三:长文本分段处理不当

有些TTS引擎对单次合成的文本长度有限制。比如一次最多合成500个字符。如果导航播报的文本超过这个限制,引擎可能会自动截断,或者返回错误。我建议你在调用TTS之前,先对长文本做分段处理。

// 长文本分段示例
private List<String> splitLongText(String text, int maxLength) {
    List<String> segments = new ArrayList<>();
    // 按标点符号分段,避免在单词中间截断
    String[] sentences = text.split("(?<=[。!?,])");
    StringBuilder current = new StringBuilder();
    for (String sentence : sentences) {
        if (current.length() + sentence.length() > maxLength) {
            segments.add(current.toString());
            current = new StringBuilder();
        }
        current.append(sentence);
    }
    if (current.length() > 0) {
        segments.add(current.toString());
    }
    return segments;
}

核心思路总结:

  • TTS无声:先看进程,再看路由,最后查回调。三步走,基本能定位90%的问题。
  • 音色异常:采样率是头号嫌疑犯,其次是语音库完整性。别被表象迷惑,先拿原始数据说话。
  • 播报中断:缓冲区管理、音频焦点抢占、长文本分段,这三个方向挨个排查。

说实话,TTS故障排查,很多时候不是技术有多难,而是你愿不愿意沉下心来看日志。我见过太多人一上来就怀疑引擎有问题,结果查了半天,发现是配置文件里一个参数写错了。嗯,干这行,细心比聪明更重要。