1.文本转语音
文本转语音
输入一段文字,输出一段音频,这就是 TTS(Text To Speech)干的事情
在开始之前,先定一个小目标:给插件传入"我喜欢吃葡萄 I like to eat grape"和选定的音色,跑完代码,收获一个能直接播放的 mp3 文件。全文就围绕这个目标展开,先讲思路,再手把手带着跑通。
I. 实现思路
文本转语音这件事,听起来很高大上,其实自己从零搞起(收集语料、训练模型、声学合成)是一个天坑,纯 Java 技术玩家基本不会走这条路。更现实的思路是:调用现成的云服务,把文字发过去,把音频流收回来。
本插件的核心实现来自开源项目 java-tts,走的就是这条路线——借用 Edge 浏览器"大声朗读"功能背后的语音合成接口:
- 通过 WebSocket 与 Edge 的语音服务建立长连接;
- 先发送"输出格式配置"(告诉服务端要 mp3 还是别的格式),再发送 SSML 报文(语音合成标记语言),带上文本、发音人(voice)、语速、音量等参数;
- 服务端以二进制帧持续回传音频数据,客户端把音频字节攒起来,收到结束标记后落盘为音频文件。
quick-media 在这个思路上做了工程化封装:把建连、协议报文、音频流的接收与保存全部收拢,对外只暴露一个链式的 TtsConfig 配置类和一个入口 TtsWrapper,用起来就剩下"传文本、选音色、拿文件"三件事。
II. 依赖引入
插件对应的 maven 坐标为(以工程 pom 为准):
<dependency>
<groupId>com.github.liuyueyi.media</groupId>
<artifactId>tts-plugin</artifactId>
<version>3.1.0</version>
</dependency>
需要说明的是,tts-plugin 是 quick-media 后期新增的插件,可能尚未同步发布到 maven 中央仓库,直接按上面的坐标依赖有可能拉不到包。如果遇到这种情况,可以按下面两种方式处理。
方式一:jitpack 仓库引入
quick-media 项目整体在 jitpack 上有发布记录,可以先添加 jitpack 仓库,再依赖(注意 jitpack 方式的 groupId 与中央仓库不同):
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.liuyueyi.quick-media</groupId>
<artifactId>tts-plugin</artifactId>
<version>3.1.0</version>
</dependency>
jitpack 的版本号以 https://jitpack.io/#liuyueyi/quick-media 上实际构建出来的为准,若该 artifact 没有对应的构建版本,请使用方式二。
方式二:本地源码安装
最稳妥的方式是把项目拉下来,进入 plugins/tts-plugin 目录执行 mvn clean install,装进本地仓库之后,上面的中央仓库坐标就能正常引用了。
插件自身的依赖
从 pom 中可以看到,tts-plugin 依赖了这些东西:base-plugin(quick-media 公共基础包,传递依赖自动引入)、okhttp + okio(WebSocket 长连接和音频字节流的承载)、mp3spi(mp3 解码,用于本地播放)、jflac-codec(flac 解码),以及 scope 为 provided 的 slf4j-api 日志门面(需要项目自己带一个日志实现)。
III. 使用姿势
1. 最小示例
整个插件对外最核心的入口是 TtsWrapper,搭配 TtsConfig 链式配置使用:
TtsWrapper.sendTxt(TtsConfig.newConfig()
.outputFileName("测试")
.setSsml("我喜欢吃葡萄 I like to eat grape")
.voice(VoiceEnum.zh_TW_HsiaoChenNeural)
.over()
);
TtsWrapper.close();
跑完之后,音频文件就躺在默认目录里了。这段代码的调用链拆开来看:
TtsConfig.newConfig():创建一个配置实例;outputFileName("测试")指定输出文件名(不带后缀,后缀由输出格式决定);setSsml(...):传入需要转语音的文本,返回SsmlConfig对象,接着可以继续配置音色(voice)、语速(rate)、音量(volume);over():回到TtsConfig,表示 ssml 配置结束;TtsWrapper.sendTxt(...):发起合成请求,TtsWrapper.close()用完之后关闭长连接释放资源。
音频默认保存路径与操作系统有关(源码 TtsWrapper 中写死的逻辑):Windows 下是 c://,其他系统是 /tmp。如果想换地方,可以:
// 单例初始化时指定
TtsWrapper.getInstance("/Users/yihui/Desktop").getTts().sendTxt(config);
// 或者直接更新全局默认路径
TtsWrapper.getInstance().updateBaseDir("/Users/yihui/Desktop");
另外 outputFileName 也支持带路径的写法(如 /tmp/music/测试),子目录不存在时会自动创建。
2. 完整参数配置
一个把常用参数都配齐的示例(来自工程测试类 Main#testTts 的扩展版):
TtsConfig config = TtsConfig.newConfig()
// 输出文件名,不传时会取文本前5个字符 + 时间戳自动生成
.outputFileName("东北话测试")
// 输出格式,不传默认 audio_24khz_48kbitrate_mono_mp3
.outputFormat(OutputFormatEnum.audio_24khz_48kbitrate_mono_mp3)
// 待转换的文本
.setSsml("老铁们,双击666")
// 音色:东北妹子晓北
.voice(VoiceEnum.zh_CN_liaoning_XiaobeiNeural)
// 语速:相对值,+50% 表示比默认快一半;也可以传相对数字如 2
.rate("+50%")
// 音量:相对值,如 +20%、-10%;绝对值 0.0 ~ 100.0 也支持
.volume("+20%")
// 结束 ssml 配置
.over()
// 合成完成后直接播放
.usePlayer();
TtsWrapper.sendTxt(config);
TtsWrapper.close();
上面这段代码会合成一段略带东北口音、语速偏快、声音略大的 mp3,并直接播放出来。
3. 用流的方式保存
默认行为是把音频写到文件。如果希望拿到 InputStream 自己处理(比如直接返回给 http 响应、上传到 OSS),可以通过 saveHook 自定义保存逻辑:
TtsConfig config = TtsConfig.newConfig()
.saveHook((b, s, t) -> {
// b: 音频字节流(ByteString) s: 保存文件名 t: 文件后缀
InputStream stream = StreamSaveHook.save(b);
// 拿到流之后想干嘛就干嘛,这里以保存文件为例
FileWriteUtil.saveFileByStream(stream, "mp3");
return null;
})
.setSsml("飞流直下三千尺 疑是银河落九天")
.voice(VoiceEnum.zh_CN_liaoning_XiaobeiNeural)
.over();
TtsWrapper.sendTxt(config);
TtsWrapper.close();
saveHook 是一个函数式接口 OutputSaveHook<T>,入参为音频字节、文件名、后缀,返回值会被原样传递;不设置时默认走 FileSaveHook,保存为本地文件并返回绝对路径。
IV. 参数说明
1. 音色选择(VoiceEnum)
音色是 TTS 里最有意思的参数。VoiceEnum 枚举内置了 300+ 个发音人,全部来自 Edge 语音服务的音色列表,覆盖了几十种语言,几个有代表性的:| 枚举值 | 说明 | | :-: | :-: | | zh_CN_XiaoxiaoNeural | 晓晓,中文女声,默认音色,活泼温暖 | | zh_CN_YunxiNeural | 云希,中文男声,活泼阳光 | | zh_CN_YunjianNeural | 云健,适合影视和体育解说 | | zh_CN_YunyeNeural | 云野,成熟男声,适合有声书 | | zh_CN_liaoning_XiaobeiNeural | 晓北,东北方言女声 | | zh_CN_shaanxi_XiaoniNeural | 晓妮,陕西方言女声 | | zh_HK_HiuMaanNeural | 曉曼,粤语女声 | | zh_TW_HsiaoChenNeural | 曉臻,台湾女声 | | en_US_AriaNeural | Aria,美式英语女声 |
voice() 支持传枚举,也支持直接传字符串(如 .voice("zh-CN-YunxiNeural"),会自动匹配枚举,匹配不到时兜底使用晓晓)。如果想在运行时动态查询有哪些音色,可以借助 LocaleService:
LocaleService service = new LocaleService();
// 按语言前缀查询,如 zh、en、ja
List<VoiceEnum> zhVoices = service.getVoiceByLan("zh");
// 按语言地区枚举查询
List<VoiceEnum> cantonese = service.getVoiceByLocale(LocaleEnum.zh_HK);
完整的音色清单,建议直接翻 VoiceEnum 源码,每个枚举值上都带了中文注释(性别、声音特点、适合场景)。此外插件还内置了 LocaleEnum(语言地区枚举,如 zh_CN、zh_HK、en_US 等上百个),是 LocaleService 查询音色时的检索维度。
2. 语速与音量(rate / volume)
SsmlConfig 上的 rate 和 volume 均支持多种写法(参考 Azure 语音服务的 prosody 规范):
- 相对百分比:
+50%、-50%、50%,表示在默认值基础上变化; - 相对数字:
rate传2表示两倍速,0.5表示一半速; - 绝对值:
volume支持0.0 ~ 100.0的数字,默认100.0。
两个参数不设置时的默认值为 +0%,即保持原样。
3. 输出格式(OutputFormatEnum)
OutputFormatEnum 定义了 Edge 语音服务支持的全部输出格式,包括 mp3、opus、ogg、pcm、webm、amr 等几十种,常用的有:
| 枚举值 | 说明 |
|---|---|
| audio_24khz_48kbitrate_mono_mp3 | 默认格式,官方也推荐 |
| audio_16khz_32kbitrate_mono_mp3 | 低码率 mp3,音质一般但文件小 |
| audio_48khz_192kbitrate_mono_mp3 | 高采样率高码率,音质更好 |
| raw_16khz_16bit_mono_pcm | 裸 pcm 流,方便做二次处理 |
一点特别提醒:输出文件的后缀是从格式名里解析出来的,比如 audio-24khz-48kbitrate-mono-mp3 解析出 mp3。而内置播放器只认 mp3 和 pcm,选了 opus、ogg 之类的格式就只做文件保存,不要开启 usePlayer(),否则播放环节会报"不支持的音频文件"。
4. 说话风格(TtsStyleEnum)
插件里还预留了一个 TtsStyleEnum,罗列了 Edge 服务支持的说话风格,如 cheerful(愉快)、sad(悲伤)、newscast(新闻播报)、poetry-reading(诗朗诵)、whispering(耳语)等三十多种。
需要说明的是,当前插件组装 SSML 报文时只消费了音色、语速、音量三个参数,style 尚未接入配置链路,这个枚举更多是作为能力参考存在;实在需要的话,可以参考 TtsConfig#toConfig 里的 SSML 模板自行扩展 mstts:express-as 标签。
5. 代理配置
插件与 Edge 语音服务走的是 wss://speech.platform.bing.com 长连接,网络受限的环境下可以配置代理:
// socks 代理,工程测试类 Main 中保留的就是这个示例
TtsConfig.newConfig().setProxy(Proxy.Type.SOCKS, "127.0.0.1", 10808);
// http 代理同理
TtsConfig.newConfig().setProxy(Proxy.Type.HTTP, "127.0.0.1", 8888);
代理不通时合成交互会直接失败,排查问题前可以先确认代理是否可用。
V. 播放器说明
如果配置了 usePlayer(),音频文件保存成功后会调用 Player.autoPlay(path),按文件后缀选择播放器:
.mp3→Mp3Player:借助mp3spi的MpegAudioFileReader把 mp3 解码为 PCM 音频流,再走播放;.pcm→PcmPlayer:直接把 PCM 数据写入本机音频输出设备(SourceDataLine);- 其他后缀 → 抛
TtsException,如上文所说,选 mp3 或 pcm 格式再开启播放最稳。
这个播放能力主要是为本地调试、demo 演示服务的(跑一下就能听到效果);服务端场景一般不需要,直接拿文件或流去对接前端 <audio> 标签。
VI. 机制补充
最后补充几个使用时容易疑惑的点,都来自源码:
TtsWrapper是单例:全局只持有一条 WebSocket 连接,多个请求复用它,效率更高;但sendTxt内部会空转等待上一次合成完成,属于串行模型;- 连接自动回收:超过 5 分钟没有新请求会主动断开长连接,下次调用时再重建;
- 文本会做转义:
setSsml里会把文本中的<、>替换为《、》,避免破坏 SSML 报文结构; - 必须调用
close():程序退出前记得关闭,否则 okhttp 的线程池和连接池不会被释放。
VII. 小结
一句话总结 tts-plugin 的定位:它把"调云服务合成语音"这件麻烦事封装成了三行代码——建连、协议、音频流都藏在 TtsWrapper + TtsConfig 背后,开箱只需要关心文本、音色和保存方式。
典型应用场景也顺手列几个:有声内容批量生成(文章、小说转音频)、消息通知播报(告警、订单状态语音提醒)、给 video/image 类多媒体产物配音。
项目地址:
更多 quick-media 系列教程,欢迎关注公众号
一灰灰blog