本文介绍 DashScope Java SDK 调用实时语音合成-通义千问时的关键接口与请求参数。
用户指南:关于模型介绍和选型建议请参见实时语音合成-通义千问。
在线体验:暂不支持。
前期准备
DashScope Java SDK 版本需要不低于2.20.9。
快速开始
server commit模式
// Java SDK 版本需要不低于2.20.9
import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Base64;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class Main {
static String[] textToSynthesize = {
"对吧~我就特别喜欢这种超市",
"尤其是过年的时候",
"去逛超市",
"就会觉得",
"超级超级开心!",
"想买好多好多的东西呢!"
};
// 实时PCM音频播放器类
public static class RealtimePcmPlayer {
private int sampleRate;
private SourceDataLine line;
private AudioFormat audioFormat;
private Thread decoderThread;
private Thread playerThread;
private AtomicBoolean stopped = new AtomicBoolean(false);
private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();
// 构造函数初始化音频格式和音频线路
public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
this.sampleRate = sampleRate;
this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
line = (SourceDataLine) AudioSystem.getLine(info);
line.open(audioFormat);
line.start();
decoderThread = new Thread(new Runnable() {
@Override
public void run() {
while (!stopped.get()) {
String b64Audio = b64AudioBuffer.poll();
if (b64Audio != null) {
byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
RawAudioBuffer.add(rawAudio);
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
playerThread = new Thread(new Runnable() {
@Override
public void run() {
while (!stopped.get()) {
byte[] rawAudio = RawAudioBuffer.poll();
if (rawAudio != null) {
try {
playChunk(rawAudio);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
decoderThread.start();
playerThread.start();
}
// 播放一个音频块并阻塞直到播放完成
private void playChunk(byte[] chunk) throws IOException, InterruptedException {
if (chunk == null || chunk.length == 0) return;
int bytesWritten = 0;
while (bytesWritten < chunk.length) {
bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
}
int audioLength = chunk.length / (this.sampleRate*2/1000);
// 等待缓冲区中的音频播放完成
Thread.sleep(audioLength - 10);
}
public void write(String b64Audio) {
b64AudioBuffer.add(b64Audio);
}
public void cancel() {
b64AudioBuffer.clear();
RawAudioBuffer.clear();
}
public void waitForComplete() throws InterruptedException {
while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
Thread.sleep(100);
}
line.drain();
}
public void shutdown() throws InterruptedException {
stopped.set(true);
decoderThread.join();
playerThread.join();
if (line != null && line.isRunning()) {
line.drain();
line.close();
}
}
}
public static void main(String[] args) throws InterruptedException, LineUnavailableException, FileNotFoundException {
QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
.model("qwen-tts-realtime")
.apikey(System.getenv("DASHSCOPE_API_KEY"))
.build();
AtomicReference<CountDownLatch> completeLatch = new AtomicReference<>(new CountDownLatch(1));
final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);
// 创建实时音频播放器实例
RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);
QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
@Override
public void onOpen() {
// 连接建立时的处理
}
@Override
public void onEvent(JsonObject message) {
String type = message.get("type").getAsString();
switch(type) {
case "session.created":
// 会话创建时的处理
break;
case "response.audio.delta":
String recvAudioB64 = message.get("delta").getAsString();
// 实时播放音频
audioPlayer.write(recvAudioB64);
break;
case "response.done":
// 响应完成时的处理
break;
case "session.finished":
// 会话结束时的处理
completeLatch.get().countDown();
default:
break;
}
}
@Override
public void onClose(int code, String reason) {
// 连接关闭时的处理
}
});
qwenTtsRef.set(qwenTtsRealtime);
try {
qwenTtsRealtime.connect();
} catch (NoApiKeyException e) {
throw new RuntimeException(e);
}
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
.voice("Chelsie")
.responseFormat(QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT)
.mode("server_commit")
.build();
qwenTtsRealtime.updateSession(config);
for (String text:textToSynthesize) {
qwenTtsRealtime.appendText(text);
Thread.sleep(100);
}
qwenTtsRealtime.finish();
completeLatch.get().await();
// 等待音频播放完成并关闭播放器
audioPlayer.waitForComplete();
audioPlayer.shutdown();
System.exit(0);
}
}
commit模式
// Dashscope SDK 版本不低于 2.20.9
import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class commit {
// 实时PCM音频播放器类
public static class RealtimePcmPlayer {
private int sampleRate;
private SourceDataLine line;
private AudioFormat audioFormat;
private Thread decoderThread;
private Thread playerThread;
private AtomicBoolean stopped = new AtomicBoolean(false);
private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();
// 构造函数初始化音频格式和音频线路
public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
this.sampleRate = sampleRate;
this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
line = (SourceDataLine) AudioSystem.getLine(info);
line.open(audioFormat);
line.start();
decoderThread = new Thread(new Runnable() {
@Override
public void run() {
while (!stopped.get()) {
String b64Audio = b64AudioBuffer.poll();
if (b64Audio != null) {
byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
RawAudioBuffer.add(rawAudio);
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
playerThread = new Thread(new Runnable() {
@Override
public void run() {
while (!stopped.get()) {
byte[] rawAudio = RawAudioBuffer.poll();
if (rawAudio != null) {
try {
playChunk(rawAudio);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
decoderThread.start();
playerThread.start();
}
// 播放一个音频块并阻塞直到播放完成
private void playChunk(byte[] chunk) throws IOException, InterruptedException {
if (chunk == null || chunk.length == 0) return;
int bytesWritten = 0;
while (bytesWritten < chunk.length) {
bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
}
int audioLength = chunk.length / (this.sampleRate*2/1000);
// 等待缓冲区中的音频播放完成
Thread.sleep(audioLength - 10);
}
public void write(String b64Audio) {
b64AudioBuffer.add(b64Audio);
}
public void cancel() {
b64AudioBuffer.clear();
RawAudioBuffer.clear();
}
public void waitForComplete() throws InterruptedException {
// 等待所有缓冲区中的音频数据播放完成
while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
Thread.sleep(100);
}
// 等待音频线路播放完成
line.drain();
}
public void shutdown() throws InterruptedException {
stopped.set(true);
decoderThread.join();
playerThread.join();
if (line != null && line.isRunning()) {
line.drain();
line.close();
}
}
}
public static void main(String[] args) throws InterruptedException, LineUnavailableException, FileNotFoundException {
Scanner scanner = new Scanner(System.in);
QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
.model("qwen-tts-realtime")
.apikey(System.getenv("DASHSCOPE_API_KEY"))
.build();
AtomicReference<CountDownLatch> completeLatch = new AtomicReference<>(new CountDownLatch(1));
// 创建实时播放器实例
RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);
final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);
QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
// File file = new File("result_24k.pcm");
// FileOutputStream fos = new FileOutputStream(file);
@Override
public void onOpen() {
System.out.println("connection opened");
System.out.println("输入文本并按Enter发送,输入'quit'退出程序");
}
@Override
public void onEvent(JsonObject message) {
String type = message.get("type").getAsString();
switch(type) {
case "session.created":
System.out.println("start session: " + message.get("session").getAsJsonObject().get("id").getAsString());
break;
case "response.audio.delta":
String recvAudioB64 = message.get("delta").getAsString();
byte[] rawAudio = Base64.getDecoder().decode(recvAudioB64);
// fos.write(rawAudio);
// 实时播放音频
audioPlayer.write(recvAudioB64);
break;
case "response.done":
System.out.println("response done");
// 等待音频播放完成
try {
audioPlayer.waitForComplete();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 为下一次输入做准备
completeLatch.get().countDown();
break;
case "session.finished":
System.out.println("session finished");
if (qwenTtsRef.get() != null) {
System.out.println("[Metric] response: " + qwenTtsRef.get().getResponseId() +
", first audio delay: " + qwenTtsRef.get().getFirstAudioDelay() + " ms");
}
completeLatch.get().countDown();
default:
break;
}
}
@Override
public void onClose(int code, String reason) {
System.out.println("connection closed code: " + code + ", reason: " + reason);
try {
// fos.close();
// 等待播放完成并关闭播放器
audioPlayer.waitForComplete();
audioPlayer.shutdown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
qwenTtsRef.set(qwenTtsRealtime);
try {
qwenTtsRealtime.connect();
} catch (NoApiKeyException e) {
throw new RuntimeException(e);
}
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
.voice("Chelsie")
.responseFormat(QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT)
.mode("commit")
.build();
qwenTtsRealtime.updateSession(config);
// 循环读取用户输入
while (true) {
System.out.print("请输入要合成的文本: ");
String text = scanner.nextLine();
// 如果用户输入quit,则退出程序
if ("quit".equalsIgnoreCase(text.trim())) {
System.out.println("正在关闭连接...");
qwenTtsRealtime.finish();
completeLatch.get().await();
break;
}
// 如果用户输入为空,跳过
if (text.trim().isEmpty()) {
continue;
}
// 重新初始化倒计时锁存器
completeLatch.set(new CountDownLatch(1));
// 发送文本
qwenTtsRealtime.appendText(text);
qwenTtsRealtime.commit();
// 等待本次合成完成
completeLatch.get().await();
}
// 清理资源
audioPlayer.waitForComplete();
audioPlayer.shutdown();
scanner.close();
System.exit(0);
}
}
访问github下载更多示例代码。
请求参数
下述请求参数可以通过QwenTtsRealtimeParam
对象的链式方法或setter配置、之后作为参数传入QwenTtsRealtime的构造方法完成配置。
参数 | 类型 | 说明 |
model | str | qwen-tts-realtime系列模型名称。参见模型列表。 |
下述请求参数可以通过QwenTtsRealtimeConfig
对象的链式方法或setter配置、之后作为参数传入updateSession接口完成配置。
参数 | 类型 | 说明 |
voice | String | 语音合成所使用的音色。 支持:"Chelsie", "Serena", "Ethan", "Cherry"。音色效果请参见:支持的音频音色。 |
output_audio_format | QwenTtsRealtimeAudioFormat | 模型输出音频的格式,当前只支持设置为PCM_24000HZ_MONO_16BIT。 |
mode | String | 使用的交互模式,可选"server_commit"和"commit",默认 sever_commit 模式。
|
关键接口
QwenTtsRealtime类
引入方法:
import com.alibaba.dashscope.audio.qwen_tts_realtime.QwenTtsRealtime;
成员方法 | 方法签名 | 服务端响应事件(通过回调下发) | 说明 |
connect |
| 会话已创建 会话配置已更新 | 和服务端创建连接。 |
updateSession |
| 会话配置已更新 | 更新本次会话交互的默认配置。参数配置请参考《请求参数》章节。 在您建立链接,服务端会及时返回用于此会话的默认输出输入配置。如果您需要更新默认会话配置,我们也推荐您总是在建立链接后即刻调用此接口。 服务端在收到session.update事件后,会进行参数校验,如果参数不合法则返回错误,否则更新服务端侧的会话配置。 |
appendText |
| 无 | 将文本片段追加到云端输入文本缓冲区。 缓冲区是你可以写入并稍后提交的临时存储。
|
clearAppendedText |
| 清空服务端收到的文本 | 删除当前云端缓冲区的文本。 |
commit |
| 提交文本并触发语音合成 响应时有新的输出内容 新的输出内容添加到assistant message 项 模型增量生成的音频 完成音频生成 Assistant mesasge 的音频内容流式输出完成 Assistant mesasge 的整个输出项流式传输完成 响应完成 | 提交之前通过append添加到云端缓冲区的文本,并立刻合成所有文本。如果输入的文本缓冲区为空将产生错误。
|
finish |
| 响应完成 | 终止任务。 |
close |
| 无 | 关闭连接。 |
getSessionId |
| 无 | 获取当前任务的session_id。 |
getResponseId |
| 无 | 获取最近一次response的response_id。 |
getFirstAudioDelay |
| 无 | 获取首包音频延迟。 |
回调接口(QwenTtsRealtimeCallback)
方法 | 参数 | 返回值 | 描述 |
| 无 | 无 | 当和服务端建立连接完成后,该方法立刻被回调。 |
| message:服务端响应事件。 | 无 | 包括对接口调用的回复响应和模型生成的文本和音频。具体可以参考:服务端事件 |
| code:关闭websokcet的状态码。 reason:关闭websocket的关闭信息。 | 无 | 当服务已经关闭连接后进行回调。 |
常见问题
Q1:输入音频的推荐频率?
A1:在实时交互场景,推荐按照100ms一包的频率发送音频。