新增 TTS 厂商开发指南
这篇文档面向需要把新 TTS 厂商接入 EasyMrcp 的开发者。
EasyMrcp 的 TTS 接入核心是 TtsEngine。新增厂商时,一般只需要新增配置类和 TTS 引擎实现,再把它注册到 ProcessorCreator.createTtsEngine()。
接入思路
可以把新增 TTS 厂商理解成“给 EasyMrcp 加一个新的发声适配器”。
EasyMrcp 已经负责了电话播放侧的事情:TTS 队列、预加载、打断、静音、RTP 打包、编码和发送。新厂商代码只需要调用厂商的合成接口,把文本变成 PCM 音频,再把 PCM 分片写回 EasyMrcp。
实际开发时按这个顺序理解最简单:
- 先确认厂商返回什么音频:采样率是多少、是否 PCM、是否流式返回。
- 再写配置:把厂商地址、密钥、模型、发音人、重采样和首尾静音裁剪参数放进
tts/*.properties。 - 再写引擎:继承
TtsEngine,实现初始化、合成播放、关闭资源。 - 最后注册入口:在
EMConstant和ProcessorCreator里加新模式,让mrcp.ttsMode或ClientConnect.TtsEngine能选到它。
整个过程的关键是不要自己发 RTP。新厂商只负责拿到音频后调用 putAudioData 写回 EasyMrcp,并在一段 TTS 结束时写入结束标志,后续播放、打断和回调都交给 EasyMrcp 主流程。
1. 推荐参考实现
可以先看这些现有实现:
src/main/java/com/cfsl/easymrcp/tts/
├─ aliyun/
│ ├─ AliyunTtsConfig.java
│ └─ AliyunCosyVoiceEngine.java
├─ xfyun/
│ ├─ XfyunTtsConfig.java
│ └─ XfyunTtsProcessor.java
├─ kokoro/
│ ├─ KokoroConfig.java
│ └─ KokoroProcessor.java
├─ tencentcloud/
│ ├─ TxCloudTtsConfig.java
│ ├─ TxCloudTtsClient.java
│ └─ TxCloudTtsProcessor.java
└─ example/
├─ ExampleTtsConfig.java
└─ ExampleTtsProcessor.java其中:
- 阿里云示例适合参考“流式合成 + 首尾静音裁剪”。
- 讯飞示例适合参考“WebSocket 鉴权和合成”。
- Kokoro 示例适合参考“HTTP 流式接口 + 24k 降采样到 8k”。
ExampleTtsProcessor适合看最小实现结构。
2. 需要改哪些文件
新增一家 TTS 厂商,一般涉及:
| 文件 | 是否必须 | 作用 |
|---|---|---|
pom.xml | 视情况 | 如果厂商有 SDK,需要增加依赖。 |
EMConstant.java | 是 | 增加新的 ttsMode 常量。 |
ProcessorCreator.java | 是 | 注册新 TTS 引擎创建逻辑。 |
tts/<vendor>/... | 是 | 新增配置类、TTS 引擎、厂商客户端。 |
src/main/resources/tts/<vendor>-tts.properties | 是 | 提供默认配置模板。 |
application.yaml | 可选 | 本地联调时切换默认 mrcp.ttsMode。 |
推荐目录:
src/main/java/com/cfsl/easymrcp/tts/<vendor>/
├─ <Vendor>TtsConfig.java
├─ <Vendor>TtsProcessor.java
└─ <Vendor>TtsClient.java
src/main/resources/tts/
└─ <vendor>-tts.properties如果厂商 SDK 很简单,也可以不单独拆 <Vendor>TtsClient.java,直接在 <Vendor>TtsProcessor 里调用。
3. 新增配置类
配置类继承 TtsConfig。
示例:
@Data
@Configuration
@ConfigurationProperties(prefix = "your-tts")
@EqualsAndHashCode(callSuper = true)
@PropertySource(
value = {"classpath:tts/your-tts.properties", "file:tts/your-tts.properties"},
ignoreResourceNotFound = true
)
public class YourTtsConfig extends TtsConfig {
private String hostUrl;
private String appId;
private String apiKey;
private String apiSecret;
private String model;
}配置文件示例:
your-tts.host-url=https://example.com/tts
your-tts.app-id=<your-app-id>
your-tts.api-key=<your-api-key>
your-tts.api-secret=<your-api-secret>
your-tts.model=<your-model>
your-tts.voice=<your-voice>
# 如果厂商返回音频开头有静音,可以按 PCM 字节数裁剪
your-tts.skipBytesInTheFirstPacket=0
# 如果厂商返回音频结尾有静音,可以按 PCM 字节数裁剪
your-tts.skipBytesInTheEndPacket=0
# 如果厂商只返回 24k PCM,而电话侧需要 8k,可以参考 Kokoro
# your-tts.re-sample=downsample24kTo8kvoice、skipBytesInTheFirstPacket、skipBytesInTheEndPacket 来自 TtsConfig,新增厂商建议保留这些通用字段。
4. 实现 TtsEngine
新厂商引擎继承 TtsEngine,需要实现三个方法:
| 方法 | 作用 |
|---|---|
create() | 初始化厂商连接或客户端。 |
speak(String text) | 调用厂商合成接口,并把 PCM 音频写回 EasyMrcp。 |
ttsClose() | 释放 WebSocket、HTTP 客户端、SDK 连接、线程池等资源。 |
最小结构:
public class YourTtsProcessor extends TtsEngine {
private final YourTtsConfig config;
public YourTtsProcessor(YourTtsConfig config) {
this.config = config;
this.voice = config.getVoice();
this.skipBytesInTheFirstPacket = config.getSkipBytesInTheFirstPacket();
}
@Override
public void create() {
// 初始化厂商客户端
}
@Override
public void speak(String text) {
// 调用厂商 TTS,把厂商返回的 PCM 分片写回 EasyMrcp
// putAudioDataWithSkip(pcmChunk, pcmChunk.length);
// 合成结束后必须写入结束标志
putAudioData(TTSConstant.TTS_END_FLAG.retainedDuplicate());
}
@Override
public void ttsClose() {
// 释放厂商客户端资源
}
}5. 音频写回规则
厂商返回音频后,不要自己发 RTP。TTS 引擎只需要把 PCM 写回 TtsHandler,后续 RTP 打包、编码、发送由 EasyMrcp 处理。
常用方法:
| 方法 | 说明 |
|---|---|
putAudioData(byte[] audioChunk, int bytesRead) | 写入一段 PCM 音频。 |
putAudioDataWithSkip(byte[] audioChunk, int bytesRead) | 写入 PCM,并在首包按 skipBytesInTheFirstPacket 裁剪。 |
putAudioData(TTSConstant.TTS_END_FLAG.retainedDuplicate()) | 写入 TTS 结束标志。 |
结束标志必须写入。否则 EasyMrcp 不知道这一段 TTS 已经合成完,后续 SpeakComplete、预加载播放和队列调度都会受影响。
6. 采样率和重采样
电话侧通常按 8k 音频播放。新增厂商时要确认厂商返回的 PCM 采样率。
常见情况:
| 厂商返回 | 处理方式 |
|---|---|
| 8k PCM | 直接写入 putAudioData。 |
| 16k PCM | 需要新增或复用降采样逻辑。 |
| 24k PCM | 可以参考 Kokoro 的 downsample24kTo8k。 |
如果配置类继承了 BaseConfig,可以在 properties 中配置:
your-tts.re-sample=downsample24kTo8k注册引擎时需要把重采样配置写给 TtsHandler:
ttsHandler.setReSample(yourTtsConfig.getReSample());7. 首尾静音裁剪
有些 TTS 厂商返回的音频首尾自带静音。EasyMrcp 支持按 PCM 字节数裁剪:
your-tts.skipBytesInTheFirstPacket=1600
your-tts.skipBytesInTheEndPacket=16008k、16bit、单声道 PCM 中:
0.1 秒 = 8000 * 2 * 0.1 = 1600 字节接入时:
- 首包裁剪在
TtsEngine.putAudioDataWithSkip()里处理。 - 尾包裁剪需要在
ProcessorCreator.createTtsEngine()中调用ttsHandler.setSkipBytesInTheEndPacket(...)。
示例:
ttsEngine = new YourTtsProcessor(yourTtsConfig);
ttsHandler.setSkipBytesInTheEndPacket(yourTtsConfig.getSkipBytesInTheEndPacket());8. 注册到 EasyMrcp
8.1 增加模式常量
在 EMConstant.java 增加:
public static final String YOUR_TTS = "your-tts";8.2 注入配置类
在 ProcessorCreator 里注入:
@Resource
YourTtsConfig yourTtsConfig;8.3 增加创建分支
在 createTtsEngine() 中增加:
case EMConstant.YOUR_TTS:
ttsEngine = new YourTtsProcessor(yourTtsConfig);
ttsHandler.setReSample(yourTtsConfig.getReSample());
ttsHandler.setSkipBytesInTheEndPacket(yourTtsConfig.getSkipBytesInTheEndPacket());
break;如果厂商不需要重采样或尾包裁剪,可以不设置对应项。
9. 切换配置并联调
默认 TTS 在 application.yaml 中配置:
mrcp:
ttsMode: your-tts也可以在 ClientConnect 时覆盖当前通话的 TTS 引擎和发音人:
{
"TtsEngine": "your-tts",
"Voice": "your-voice"
}联调时至少验证:
Speak能触发厂商合成。- 音频能通过 RTP 播放出来。
- 结束时能收到
SpeakComplete。 Interrupt/InterruptAndSpeak能打断当前播放。- 连续多段
Speak不串音。 - 预加载 TTS 能正常播放下一段。
- 通话结束时
ttsClose()会释放资源。
10. 常见问题
| 现象 | 排查方向 |
|---|---|
| 只有第一段能播,后续卡住 | 检查是否写入 TTS_END_FLAG。 |
| 播放速度或音调异常 | 检查厂商采样率和 re-sample。 |
| 每段 TTS 前后停顿明显 | 检查 skipBytesInTheFirstPacket 和 skipBytesInTheEndPacket。 |
| 打断后还有旧音频播放 | 检查是否使用 putAudioData 走 EasyMrcp 的版本号和队列控制,不要绕过 TtsHandler 自己发 RTP。 |
| 预加载音频没有播放 | 检查结束标志、ttsVersion 和预加载路径是否正常写入缓存。 |
| 通话结束后线程不退出 | 检查 ttsClose() 是否关闭厂商连接和线程池。 |
