简介:freeTTS是一个基于Java的开源文本转语音系统,面向需要为应用增加语音播报能力的Java开发者,可灵活用于语音助手、教育软件、车载导航、自动电话服务和无障碍辅助等场景。压缩包共包含103个文件,大小约3.75MB,核心以java源码、txt说明、sh脚本与utt语音测试文件为主,并附带metrics性能统计、dat数据及html文档,不仅覆盖合成器、发音选择、文本处理和事件驱动API等核心模块,还提供跨平台运行所需的配置、测试脚本与示例语料。已有800人学习下载,适合具备基础Java知识、希望快速集成TTS或深入研究语音合成机制的开发者。包内代码遵循GNU自由软件许可,可直接用于工程实践,也可自由修改与再分发;配合示例脚本和性能统计数据,能帮助理解freeTTS的调用方式与内部实现,为二次开发、教学演示或语音方案选型提供完整素材。
1. FreeTTS 是低调的 java 语音包:不接外部服务也能把文字变成声音
在Java项目中让程序开口说话,最省事的方案不是接云服务,而是一个存在了很多年的java语音包freeTTS。它是纯Java写的文本转语音引擎,本地就能完成从字符串到wav的合成,不需要网络和Key,适合做提示音、语音播报、离线朗读工具。这篇文章按我实际拆包的顺序,从依赖引入、最小代码、语速音调到最容易翻车的并发问题,把整个资源讲透。新手能照着复制,熟手可以只挑避坑部分看。
2. 先看透 FreeTTS 的骨架:模块、声音机制与依赖引入
2.1 模块组成:它如何把文本变成音素和波形
FreeTTS移植自Flite,一个用C写的轻量TTS引擎,核心目标只解决“把文字变成音频流”。它不做自然语言理解,也没有情感标记,但正因如此,启动快、无网络请求、无按量计费,适合工具型功能。
整个库可以按三个模块来理解。VoiceManager负责管理所有声音对象,你可以把它理解成声音注册表,通过它枚举当前可用语音、按名称取Voice。Voice是真实的声音句柄,封装了音高、语速、音量、停顿等参数,以及合成动作。Voice背后对应一套发音字典和一套声学模型,换句话说,它决定“用谁的嗓子说话”。最底层是Synthesizer部分,负责真正生成线性预测编码波形,再把波形编码成16位音频。
工作流程大致是:输入文本 → 分词和归一化,把数字、缩写展开成自然语言 → 语义处理,查字典并转换成内部音素表达 → 音素参数设置 → 波形采样 → 编码为WAV或送往当前音频播放器。其中“文本归一化”对英文语料处理得比较成熟,它能正确读出“123 roads”这类表达,但把中文原文直接塞进去就会在这一层出现问题——分词和音素映射完全不在它的知识范围内。这是一个关键边界,后文会专门展开。
为什么要单独说模块?因为很多人在引入这个库时,只关心“哪个方法能出声音”,忽略了它是一个有状态的内部引擎。后续遇到并发卡死、声音资源找不到、参数不生效时,回到模块边界去看反而更快定位。
2.2 常见的声音数据包与语言边界
资源包里通常有多个jar,体积差异主要来自各自的声学模型和词典。常用几种声音包如下:
| jar 文件 | 嗓音名称 | 特征与适用场景 |
|---|---|---|
| cmu_us_kal.jar | kevin | 16kHz采样,音质偏机械,适合极简状态音 |
| cmu_us_kal16.jar | kevin16 | 同说话人但清晰度更好,最常见通用选择 |
| cmu_us_slt.jar | slt | 女声,适合导航播报、朗读句子 |
| cmu_time_awb.jar | awb | 另一种说话人,风格偏严肃 |
这些整包全部是英文说话人,没有内置中文语音。要做中文合成,常见做法是先把中文转成带声调的拼音,再用这些音素去喂给FreeTTS;或者直接用其他专门的中文TTS引擎。我一般只在工具类应用里使用FreeTTS处理英文播报、状态码、倒计时和编号朗读。
注意
不要幻想把所有jar一次性塞进classpath就能获得多语言多音色。FreeTTS通常只加载当前可识别到的第一套声音数据包,其余会被忽略。多个jar之间的注册名如果冲突,还会导致枚举列表不全。
2.3 把 jar 正确装进工程:从本地仓库到 system 依赖
第一件需要接受的事是:FreeTTS不在Maven Central仓库,你拿不到现成坐标。只能在本地把jar手动装进仓库,或者用system scope指明路径。
方法一:用Maven本地安装命令,把两个核心jar装进本机仓库。先拿到freetts.jar和对应的语音包jar,再执行:
mvn install:install-file -Dfile=lib/freetts.jar -DgroupId=com.freetts -DartifactId=freetts -Dversion=1.2.2 -Dpackaging=jar mvn install:install-file -Dfile=lib/cmu_us_kal16.jar -DgroupId=com.freetts -DartifactId=freetts-en-us -Dversion=1.2.2 -Dpackaging=jar逻辑说明:install-file插件的作用是把不在中央仓库的jar变成带groupId、artifactId、version的本地坐标。后续pom文件里声明依赖时,Maven会优先从本地仓库解析。这个方案适合单机研究,但换机器或多人协作时,还需要把坐标同步到一个企业级私有仓库。
参数说明:Dfile必须是jar的真实路径;DgroupId和DartifactId会拼出仓库路径,比如com/freetts/freetts/1.2.2/;Dpackaging默认就是jar,不写也行,但明确写出来能避免部分IDE误判。
方法二:用system scope直接指向工程内lib目录。配置简单,但比较危险:
<dependency> <groupId>com.freetts</groupId> <artifactId>freetts</artifactId> <version>1.2.2</version> <scope>system</scope> <systemPath>${project.basedir}/lib/freetts.jar</systemPath> </dependency> <dependency> <groupId>com.freetts</groupId> <artifactId>freetts-en-us</artifactId> <version>1.2.2</version> <scope>system</scope> <systemPath>${project.basedir}/lib/cmu_us_kal16.jar</systemPath> </dependency>这段配置能编译,但打包成可执行jar时,system依赖很容易被漏掉。我见过不少同事开发环境跑得好好的,一打包运行就抛NoClassDefFoundError,根源就在这里。
除Maven外,IDE里用普通Java项目的人也很多,直接把freetts.jar和语音包jar加入项目Library即可。无论哪种方式,引入完成后不要急着写业务逻辑,先跑一个枚举Voice的小程序,确认环境真的通了。
这里还要明确选型边界。如果你的业务对语音自然度要求很高,需要多音色或包含情感,FreeTTS不合适,云TTS更现实。但反过来,如果你只需要状态播报、倒计时提醒、英文单词朗读,FreeTTS的优势是启动快、不用联网、无按量计费。它的合成质量停留在上一代水平,但拿去做功能里的“听感配角”完全够用。
3. 动手合成第一段语音:最小示例、参数打磨与落地
3.1 最小可运行:枚举声音并合成 WAV 文件
从零跑通需要四步:初始化VoiceManager,拿到Voice或按名字取Voice,allocate占资源,最后合成到文件。
import com.sun.speech.freetts.Voice; import com.sun.speech.freetts.VoiceManager; public class TtsDemo { public static void main(String[] args) { // 1. 初始化语音管理器 VoiceManager voiceManager = VoiceManager.getInstance(); // 2. 枚举当前实际加载的声音名 Voice[] voices = voiceManager.getVoices(); for (Voice v : voices) { System.out.println("voice name = " + v.getName()); } // 3. 用名字取一个 Voice,常见的是 kevin16 Voice voice = voiceManager.getVoice("kevin16"); if (voice == null) { throw new IllegalStateException("voice not found, 检查是否把语音包 jar 加入依赖"); } voice.allocate(); // 4. 合成文本到 WAV 文件,返回布尔值表示是否成功 boolean generated = voice.synthesizeToFile( "Hello, this is FreeTTS speaking.", "output.wav" ); System.out.println("generated = " + generated); voice.deallocate(); } }运行后目录里应该出现output.wav,用播放器能听到那句英文。这里的核心调用是synthesizeToFile,它内部会走完整合成流水线并输出PCM数据。
逻辑说明:voice.allocate()负责加载发音字典和声学模型参数,这一步比较耗时,应在需要时调用,不要每次合成都重新分配。deallocate在合成结束后释放内存引用。getVoices()返回的是当前classpath下实际能扫描到的声音,通常只有一个,因为多数项目只引入了一组语音包。
参数说明:kevin16本身不是一个版本号,而是cmu_us_kal16声音包内部注册的说话人名字。不同分包形式下,还可能出现kevin、alan、slt这类名字。所以不要硬编码声音名,先枚举一次再决定。
3.2 调音参数:语速、音量、音调与停顿
Voice提供了几个直接修改合成的set方法,参数范围比很多人想的小,先用一组典型值感受一下。
voice.setRate(180f); // 语速,词/分钟 voice.setPitch(100f); // 基频,默认100Hz附近 voice.setVolume(1.0f); // 音量,0-1 voice.setPitchRange(8f); // 音调起伏幅度 voice.setPitchShift(1.0f); // 整体音调偏移 voice.setDurationStretch(1.0f); // 时长拉伸系数- rate默认在130左右,范围约90到300。低于100会拖沓,高于250容易吞字。
- volume是主观的,0.1和0.5差异很大,超过1.0会削波失真。
- pitch是出声的基频底线,男声一般用100,女声可以试170。
- pitchShift调整整体音调高低,适合做机械感效果。
我在做倒计时提示音时常用组合是:rate=160、pitch=100、volume=0.8、pitchRange=6。合成出来是清晰但略带机械感的播报风格,比默认参数更适合设备音。
有一点容易误认:这里的pitch是基础音高,不是语言里的声调。用它模拟中文四声只会得到反效果,别在这上面花时间。
synthesizeToFile本身没有提供边合成边播放的通道。如果你需要实时放音,就要把AudioPlayer换成播放器并调用voice.speak。
import com.sun.speech.freetts.audio.AudioPlayer; import com.sun.speech.freetts.audio.SingleFileAudioPlayer; AudioPlayer player = new SingleFileAudioPlayer("live", javax.sound.sampled.AudioFileFormat.Type.WAVE); voice.setAudioPlayer(player); voice.speak("Playing this text in real time."); player.close();SingleFileAudioPlayer会把合成结果实时写入live.wav,能与speak方法协作,让你感知流式写入。这种写法适合语音可随时被打断的交互场景。
3.3 从短文本到长文本拼接:合成大段内容的工程化写法
直接用synthesizeToFile传一大段文本,超过500字符后就容易出现生硬断句。更稳妥的做法是先拆分句子,逐句合成,再拼成完整文件。
String scriptText = Files.readString(Path.of("script.txt")); String[] parts = scriptText.split("(?<=[.!?])\\s+"); AudioPlayer player = new SingleFileAudioPlayer("merged", javax.sound.sampled.AudioFileFormat.Type.WAVE); Voice voice = voiceManager.getVoice("kevin16"); voice.setAudioPlayer(player); voice.allocate(); for (String part : parts) { if (part.isBlank()) continue; voice.speak(part); // 内部会追加到同一个 player } player.close(); voice.deallocate();逻辑说明:split使用的正则是(?<=[.!?])\s+,意思是“在标点之后并且后面有空白的位置拆分”,这样能保留句子结尾的标点,避免被切丢。speak被串行调用,每句话的音频按顺序写入同一个SingleFileAudioPlayer,最后得到完整文件。
参数说明:如果句子之间希望加停顿,可以在两句之间插入一句极短的“silence”或调整标点数量。有些版本支持通过文本占位符插入停顿,但效果不如直接控制split策略可靠。
注意
合成完长文本后控制台出现“Done. Wrote 12.34 seconds of audio”,这是SingleFileAudioPlayer关闭时的正常确认,表示数据已flush,不是副作用警告。
4. 避坑实录:FreeTTS 最容易翻车的五个细节
4.1 找不到 Voice:明明装了数据包却仍抛异常
现象:VoiceManager.getVoice("kevin16")返回null,或者直接抛IllegalStateException,提示找不到合适的voice。环境变量里明显有freetts.jar和语音包jar,看起来什么都没缺。
原因:FreeTTS通过扫描classpath里的声音数据包MANIFEST.MF来注册voice。如果把语音包jar放在lib目录但没被构建工具视为依赖,或者打包成胖jar时META-INF条目被覆盖,声音数据就不在运行时classpath中,自然找不到voide。
解决:先看getVoices()输出。如果返回值是空数组,问题一定在语音包加载。Maven工程用shade插件合并多个jar时,特别容易出现META-INF资源互相覆盖,导致注册表被冲掉。我处理这类场景的方法是:保留原始jar依赖,在shade配置里排除重复的META-INF文件,再单独把声音jar复制到运行目录,用classpath通配符启动。
4.2 中文文本成乱码:原生语音包的边界在哪
现象:传入“你好”,程序不报错,但合成出来类似“ni hao”的奇怪发音,甚至直接没有声音。部分输入会出现不自然的音节断裂。
原因:FreeTTS整个文本处理管线是为英文设计的,内部词典没有中文字符与拼音的协同映射。它能做的是音素级别转写,但中文的声调和多音字问题需要外部预处理,原生完全没有。
解决:要想获得可听的中文播报,不要指望直接传中文原文。先做一层转换,把中文转成带声调拼音,再把这串拼音作为输入交给FreeTTS,同时用pitchShift和rate做补偿。这样做出来的音色偏机械,但至少能听懂。如果你需要标准普通话,直接换专门的中文引擎,别硬套。
4.3 并发合成卡死:同一个 Voice 对象别共享
现象:在Web应用里,两个请求同时调用voice.speak,一个正常,另一个阻塞直到超时,整个线程池被拖垮。偶尔不是卡死,而是两个声音叠在一起输出。
原因:Voice对象不是线程安全的。它内部持有合成器状态,第二次调用会覆盖第一次的上下文。allocate和deallocate也不是可重入操作,多线程共享一个Voice时资源竞争会非常明显。
解决:要么给每个线程独立创建Voice,要么把合成任务串行化。最省事的写法是用单一Voice加锁串行,所有请求排队通过。对于需要即时并发的场景,可以按线程数量创建一个Voice对象池,每次取用后在finally里deallocate。我在项目里对合成服务加了文件结果缓存,同一文本只合成一次,第二次直接读缓存,既避免并发竞争,又省CPU。
4.4 WAV 写出来不能播:关闭流与采样格式的玄学
现象:程序提示合成成功,output.wav也有几十KB,但播放器报格式错误,或者时长只有0.几秒。
原因:常见两个问题。第一,SingleFileAudioPlayer没有调用close,文件结尾的DATA chunk没写完,播放器读到一半就断。第二,FreeTTS默认输出采样率是8kHz/16bit,部分播放器不支持这个低采样率,导致打不开。
解决:把player.close()放到最后一步并确认被调用。如果嫌弃8kHz音质太差,可以显式指定采样格式再创建音频播放器。
javax.sound.sampled.AudioFormat format = new javax.sound.sampled.AudioFormat( 16000, 16, 1, true, false); AudioPlayer player = new com.sun.speech.freetts.audio.SingleFileAudioPlayer( "out", javax.sound.sampled.AudioFileFormat.Type.WAVE);这里把PCM格式指定为16kHz、16位、单声道,文件体积约翻倍,但兼容性大幅提升。播放端如果再报错,优先怀疑播放器解码能力,而不要马上怀疑文件本身。
4.5 Maven 工程放不进中央仓库:依赖引入的坑
现象:同事拉下工程后执行mvn clean package失败,报找不到com.freetts:freetts:1.2.2。本机构建一切正常。
原因:本地仓库里的坐标只是手动install出来的,没有同步到团队私有仓库。文档里虽然写了依赖坐标,但没交代这个坐标是本地生成的结果,别的人自然会拉不到。
解决:把install命令写成脚本放到项目res目录,拉代码后先执行一次。更规范的做法是配置Nexus或Artifactory,用deploy插件把jar上传到公司内部仓库,这样所有成员不需要手动操作。
我自己第二次做这个项目时,直接把FreeTTS封装成了内部共享模块,依赖坐标写在二方库里,才彻底告别“换机器就要重新install”。如果只是个人研究,方法一的install命令已经足够。
5. 封一层服务给业务用:安全锁与验证技巧
5.1 封装合成服务,强制串行和缓存
给业务方使用时,我不让代码直接碰Voice。封装一个TtsService,把内部锁、缓存、文件名规范全部收口,对外只暴露最少的合成方法。
public class TtsService { private final Voice voice; private final Path outDir; public TtsService(Path outDir) { this.outDir = outDir; VoiceManager vm = VoiceManager.getInstance(); this.voice = vm.getVoice("kevin16"); if (this.voice == null) { throw new IllegalStateException("voice not init"); } this.voice.allocate(); } public synchronized Path synthesize(String text) throws IOException { String fileName = Integer.toHexString(text.hashCode()) + ".wav"; Path target = outDir.resolve(fileName); if (Files.exists(target)) return target; boolean ok = voice.synthesizeToFile(text, target.toString()); if (!ok) throw new IOException("synthesis failed"); return target; } }逻辑说明:把synchronized加在方法上,本质就是强制合成过程串行化,防止多线程同时占用Voice。缓存键用文本hashCode,相同内容直接复用已有文件,避免重复合成和重复占CPU。
参数说明:如果请求量更大,可以把voice改成ThreadLocal,每个线程独立持有一个Voice,但运行时内存会随之上升。我观察过两个Voice实例的常驻内存增量在几十MB量级,小服务器还能接受。
5.2 验证技巧:不能光听,还要看文件头
最终交付前,我会写一个很小的验证脚本去检查WAV文件头,不只靠人耳判断。判断逻辑很简单:打开文件头前44字节,确认RIFF和WAVE标记,再读采样率、声道数、位深和data chunk长度。
file output.wav # 期望输出: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 16000 Hz如果用Java,也可以直接读取AudioInputStream验证:
AudioInputStream in = AudioSystem.getAudioInputStream(target); System.out.println("format=" + in.getFormat()); System.out.println("frameLength=" + in.getFrameLength()); long seconds = in.getFrameLength() / (long) in.getFormat().getFrameRate(); System.out.println("duration=" + seconds + "s");验证时我会容忍时长误差,但严格检查采样率是不是按配置来的。FreeTTS对不同声音包的声道定义不完全一致,换成其他声音时可能变双声道,导致文件体积翻倍,缓存逻辑也要跟着调整。
最后分享一个个人习惯。每次调音或更换语音包,我会先合成一秒钟的测试音,用file命令打印格式,再刷新缓存目录重听。不要跳过前两步,否则中间混进了格式错误,后面所有业务音频解析都会跟着乱。从那以后,我每次给FreeTTS加新功能,都强制走一遍“枚举voice、短句合成、检查文件头、听效果”的流程,这套流程帮我挡住过好几次低级问题。希望帮到你。
本文还有配套的精品资源,点击获取