You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

蓝牙连接时音频出现间隙问题的排查与解决求助

问题描述

我使用SSML实现应用的语音播报功能,本地播放完全正常,但手机连接蓝牙设备后,音频频繁出现间隙或延迟,甚至会丢失开头或中间的内容。比如原音频是Hello John, I am your assistant. How can I help you?,蓝牙播放时可能变成sistant. How can I help you?,偶尔播放流畅,但问题时常发生。

排除蓝牙距离问题,设备紧邻。由于Google SSML需要网络,不确定是蓝牙还是网络导致的问题,希望找到方法消除间隙,确保音频完全准备好再播放。

现有代码

播放音频的核心代码:

String myFile = context.getFilesDir() + "/output.mp3";
mMediaPlayer.reset();
mMediaPlayer.setDataSource(myFile);
mMediaPlayer.prepare();
mMediaPlayer.start();

完整TTS实现类:

public class Tts {
    public Context context;
    private final MediaPlayer mMediaPlayer;

    public Tts(Context context, MediaPlayer mMediaPlayer) {
        this.context = context;
        this.mMediaPlayer = mMediaPlayer;
    }

    @SuppressLint({"NewApi", "ResourceType", "UseCompatLoadingForColorStateLists"})
    public void say(String text) throws Exception {
        InputStream stream = context.getResources().openRawResource(R.raw.credential); // R.raw.credential是credential.json
        GoogleCredentials credentials = GoogleCredentials.fromStream(stream);
        TextToSpeechSettings textToSpeechSettings =
                TextToSpeechSettings.newBuilder()
                        .setCredentialsProvider(
                                FixedCredentialsProvider.create(credentials)
                        ).build();


        // 初始化客户端
        try (TextToSpeechClient textToSpeechClient = TextToSpeechClient.create(textToSpeechSettings)) {

            // 替换{name}为目标名称
            SharedPreferences sharedPreferences = context.getSharedPreferences("target", Context.MODE_PRIVATE);
            String target = sharedPreferences.getString("target", null);
            text = (target != null) ? text.replace("{name}", target) : text.replace("null", "");

            // 设置待合成的文本输入
            String myString = "<speak><prosody pitch=\"low\">" + text + "</prosody></speak>";
            SynthesisInput input = SynthesisInput.newBuilder().setSsml(myString).build();

            // 构建语音请求,选择语言代码和SSML语音性别
            VoiceSelectionParams voice =
                    VoiceSelectionParams.newBuilder()
                            .setName("de-DE-Wavenet-E")
                            .setLanguageCode("de-DE")
                            .setSsmlGender(SsmlVoiceGender.MALE)
                            .build();

            // 选择返回的音频文件类型
            AudioConfig audioConfig =
                    AudioConfig.newBuilder().setAudioEncoding(AudioEncoding.MP3).build();

            // 执行文本转语音请求
            SynthesizeSpeechResponse response = textToSpeechClient.synthesizeSpeech(input, voice, audioConfig);

            // 获取音频内容
            ByteString audioContents = response.getAudioContent();

            // 将音频写入本地文件
            try (FileOutputStream out = new FileOutputStream(context.getFilesDir() + "/output.mp3")) {
                out.write(audioContents.toByteArray());
            }

            String myFile = context.getFilesDir() + "/output.mp3";
            mMediaPlayer.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build());
            mMediaPlayer.reset();
            mMediaPlayer.setDataSource(myFile);
            mMediaPlayer.prepare();
            mMediaPlayer.setOnPreparedListener(mediaPlayer -> mMediaPlayer.start());
        }
    }
}

已尝试的无效方案

  • 设置音频类型为语音:
    mMediaPlayer.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build());
    
  • 替换mMediaPlayer.prepare()为mMediaPlayer.prepareAsync(),但音频无法播放
  • 在OnPreparedListener中调用start(),间隙问题仍存在

解决方案

1. 优化音频编码参数,适配蓝牙传输

Google TTS生成的默认MP3参数可能与部分蓝牙设备不兼容,导致解码丢包。调整AudioConfig使用更适合语音传输的配置:

// 方案1:改用无压缩PCM格式,减少蓝牙解码开销
AudioConfig audioConfig = AudioConfig.newBuilder()
        .setAudioEncoding(AudioEncoding.LINEAR16)
        .setSampleRateHertz(16000) // 语音常用采样率,降低传输压力
        .build();

// 方案2:如果坚持用MP3,指定低比特率
AudioConfig audioConfig = AudioConfig.newBuilder()
        .setAudioEncoding(AudioEncoding.MP3)
        .setSampleRateHertz(16000)
        .setAudioBitrate(32000) // 32kbps足够语音清晰,减少数据量
        .build();

2. 修正MediaPlayer播放逻辑,确保完全就绪后启动

当前代码中OnPreparedListener的注册时机错误,应在prepare()前设置,避免监听器未生效就完成准备:

String myFile = context.getFilesDir() + "/output.mp3";
mMediaPlayer.reset();

// 先注册准备完成监听器
mMediaPlayer.setOnPreparedListener(mediaPlayer -> {
    if (mediaPlayer.isPrepared()) {
        mediaPlayer.start();
    }
});

// 配置音频属性为助手语音,系统会优化蓝牙传输优先级
mMediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
        .setUsage(AudioAttributes.USAGE_ASSISTANT)
        .build());

mMediaPlayer.setDataSource(myFile);
mMediaPlayer.prepare();

如果使用prepareAsync(),需确保监听器正确注册,且添加错误监听排查问题:

mMediaPlayer.reset();
mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.start());
mMediaPlayer.setOnErrorListener((mediaPlayer, what, extra) -> {
    Log.e("TTS", "MediaPlayer错误: " + what + ", " + extra);
    return false;
});
mMediaPlayer.setAudioAttributes(/* ... */);
mMediaPlayer.setDataSource(myFile);
mMediaPlayer.prepareAsync();

3. 避免音频文件覆盖冲突

每次生成音频都覆盖同一文件,若前一次播放未结束就写入新内容,可能导致文件损坏。改用唯一文件名:

// 生成带时间戳的唯一文件名
String uniqueFileName = context.getFilesDir() + "/output_" + System.currentTimeMillis() + ".mp3";
try (FileOutputStream out = new FileOutputStream(uniqueFileName)) {
    out.write(audioContents.toByteArray());
}

// 使用唯一文件播放
mMediaPlayer.setDataSource(uniqueFileName);

4. 优化系统蓝牙设置

  • 引导用户关闭蓝牙设备的"高清音频"模式(如AAC/SBC HD),部分设备在该模式下会增加延迟和丢包
  • 在Manifest中添加权限,确保应用能修改音频设置:
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    

5. 排查网络因素

提前下载好音频文件,离线测试蓝牙播放。如果离线播放仍有问题,说明是蓝牙适配问题;若正常,则需优化TTS音频生成后的写入逻辑,确保文件完全写入后再触发播放。

内容的提问来源于stack exchange,提问作者xRay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 12:03:22