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

Java聊天应用FreeTTS多语音同时播放异常问题求助

解决FreeTTS多线程同时语音播放的问题

你遇到的核心问题是FreeTTS的底层设计限制——默认的语音合成组件(比如CMU的Kevin语音)并非线程安全,且音频输出通道默认共享,这就导致多实例并发运行时,不仅会出现音频抢占(只能一个声音播放),还会触发Syllable relation has already been set这类资源冲突错误。下面给你两个可行的解决方案:

方案1:修复现有代码,确保合成器实例完全隔离并正确管理资源

你的当前代码有几个可以优化的点,调整后能有效缓解甚至解决问题:

1.1 只初始化一次引擎中心

Central.registerEngineCentral只需要在应用启动时执行一次,重复注册会导致不必要的资源冲突。可以把这段初始化逻辑放到枚举的静态块中:

@Slf4j
public enum Speech implements SpeechAgent {
    Instance;

    private static Random pool = new Random();

    // 静态块初始化引擎,仅执行一次
    static {
        System.setProperty("freetts.voices", "com.sun.speech.freetts.en.us.cmu_us_kal.KevinVoiceDirectory");
        Central.registerEngineCentral("com.sun.speech.freetts.jsapi.FreeTTSEngineCentral");
    }

    private static Synthesizer createSynthesizer() {
        try {
            SynthesizerModeDesc desc = new SynthesizerModeDesc(Locale.US);
            Synthesizer synthesizer = Central.createSynthesizer(desc);
            synthesizer.allocate();
            synthesizer.resume();
            
            Voice[] voices = null;
            while (voices == null) {
                voices = ((SynthesizerModeDesc) synthesizer.getEngineModeDesc()).getVoices();
            }
            synthesizer.getSynthesizerProperties().setVoice(voices[pool.nextInt(voices.length)]);
            
            // 为每个合成器配置独立的音频播放器,避免音频抢占
            com.sun.speech.freetts.jsapi.FreeTTSSynthesizer freeTtsSyn = 
                (com.sun.speech.freetts.jsapi.FreeTTSSynthesizer) synthesizer;
            freeTtsSyn.getVoice().setAudioPlayer(new com.sun.speech.freetts.audio.JavaAudioPlayer());
            
            return synthesizer;
        } catch (EngineException | AudioException | EngineStateError | PropertyVetoException e) {
            log.error("Failed to load synthesizer: {}", e.toString());
        }
        return null;
    }

    public void speak(String input) {
        new Thread(new Runnable() {
            public void run() {
                Synthesizer synthesizer = createSynthesizer();
                if (synthesizer == null) {
                    return;
                }
                try {
                    String cleanInput = input.replaceAll("[^a-zA-Z ]+", "");
                    synthesizer.speakPlainText(cleanInput, null);
                    // 等待语音合成完成再释放资源
                    synthesizer.waitEngineState(Synthesizer.QUEUE_EMPTY);
                } catch (InterruptedException | EngineStateError e) {
                    log.error("Error during speech synthesis: {}", e.toString());
                } finally {
                    // 必须释放合成器资源,避免内存泄漏
                    try {
                        synthesizer.deallocate();
                    } catch (EngineException | EngineStateError e) {
                        log.error("Failed to deallocate synthesizer: {}", e.toString());
                    }
                }
            }
        }).start();
    }
}

1.2 关键优化点说明

  • 把引擎注册逻辑移到静态块,避免重复初始化导致的资源冲突
  • 为每个合成器实例配置独立的JavaAudioPlayer,解决音频输出抢占问题
  • 添加waitEngineState(Synthesizer.QUEUE_EMPTY)确保语音播放完成后再释放资源
  • 在finally块中强制释放合成器,避免内存泄漏

方案2:切换到更现代的TTS引擎(推荐)

FreeTTS是比较老旧的项目,对多线程并发的支持有限。如果方案1效果达不到预期,建议切换到更适合并发场景的开源TTS引擎:

  • MaryTTS:纯Java实现的开源TTS引擎,原生支持多实例并发,线程安全保障更完善,还支持多语言和多种语音音色
  • PicoTTS:轻量级嵌入式TTS引擎,Java可以通过JNI调用,资源占用低,并发表现稳定

关于错误的补充解释

java.lang.IllegalStateException: Syllable relation has already been set这个错误,是因为FreeTTS的Segmenter音节处理组件内部使用了共享状态变量,当多个线程同时调用时,会导致状态被重复修改,从而触发异常。通过确保每个合成器实例完全独立(包括底层的Voice和处理组件),就能避免这类问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:10:11