本文介紹 DashScope Java SDK 調用即時語音合成時的關鍵介面與請求參數。
前期準備
DashScope Java SDK 版本需要不低於2.22.7。
快速開始
server commit模式
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.*;
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 = {
"對吧~我就特別喜歡這種超市",
"尤其是過年的時候",
"去逛超市",
"就會覺得",
"超級超級開心!",
"想買好多好多的東西呢!"
};
public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;
// 即時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<>();
private ByteArrayOutputStream totalAudioStream = new ByteArrayOutputStream();
// 建構函式初始化音頻格式和音頻線路
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);
// 將音頻資料寫入 totalAudioStream
try {
totalAudioStream.write(rawAudio);
} catch (IOException e) {
throw new RuntimeException(e);
}
} 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, IOException {
stopped.set(true);
decoderThread.join();
playerThread.join();
// 儲存完整音頻檔案
File file = new File("TotalAudio_"+ttsFormat.getSampleRate()+"."+ttsFormat.getFormat());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(totalAudioStream.toByteArray());
}
if (line != null && line.isRunning()) {
line.drain();
line.close();
}
}
}
public static void main(String[] args) throws InterruptedException, LineUnavailableException, IOException {
QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
// 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime
.model("qwen3-tts-flash-realtime")
// 以下為新加坡地區的配置。
.url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
.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":
// 會話建立時的處理
if (message.has("session")) {
String eventId = message.get("event_id").getAsString();
String sessionId = message.get("session").getAsJsonObject().get("id").getAsString();
System.out.println("[onEvent] session.created, session_id: "
+ sessionId + ", event_id: " + eventId);
}
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("Cherry")
.responseFormat(ttsFormat)
.mode("server_commit")
// 如需使用指令控制功能,請取消下方注釋,並將model替換為qwen3-tts-instruct-flash-realtime
// .instructions("")
// .optimizeInstructions(true)
.build();
qwenTtsRealtime.updateSession(config);
for (String text:textToSynthesize) {
qwenTtsRealtime.appendText(text);
Thread.sleep(100);
}
qwenTtsRealtime.finish();
completeLatch.get().await();
qwenTtsRealtime.close();
// 等待音頻播放完成並關閉播放器
audioPlayer.waitForComplete();
audioPlayer.shutdown();
System.exit(0);
}
}
commit模式
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.*;
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 Main {
public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;
// 即時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<>();
private ByteArrayOutputStream totalAudioStream = new ByteArrayOutputStream();
// 建構函式初始化音頻格式和音頻線路
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);
// 將音頻資料寫入 totalAudioStream
try {
totalAudioStream.write(rawAudio);
} catch (IOException e) {
throw new RuntimeException(e);
}
} 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();
// 儲存完整音頻檔案
File file = new File("TotalAudio_"+ttsFormat.getSampleRate()+"."+ttsFormat.getFormat());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(totalAudioStream.toByteArray());
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
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替換為qwen3-tts-instruct-flash-realtime
.model("qwen3-tts-flash-realtime")
// 以下為新加坡地區的配置。
.url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
.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("Cherry")
.responseFormat(ttsFormat)
.mode("commit")
// 如需使用指令控制功能,請取消下方注釋,並將model替換為qwen3-tts-instruct-flash-realtime
// .instructions("")
// .optimizeInstructions(true)
.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 |
String |
是 |
模型名稱。參見支援的模型。 |
|
url |
String |
是 |
華北2(北京)地區: 新加坡地區: |
下述請求參數可以通過QwenTtsRealtimeConfig對象的鏈式方法或setter配置、之後作為參數傳入updateSession介面完成配置。
|
參數 |
類型 |
是否必須 |
說明 |
|
voice |
String |
是 |
語音合成所使用的音色。參見支援的音色。 支援系統音色和專屬音色:
|
|
languageType |
String |
否 |
指定合成音訊語種,預設為
|
|
mode |
String |
否 |
互動模式,可選值:
|
|
format |
String |
否 |
模型輸出音訊格式。 支援的格式:
千問-TTS-Realtime(參見支援的模型)僅支援 |
|
sampleRate |
int |
否 |
模型輸出音訊採樣率(Hz)。 支援的採樣率:
千問-TTS-Realtime(參見支援的模型)僅支援24000。 |
|
speechRate |
float |
否 |
音訊語速。1.0為正常語速,小於1.0為慢速,大於1.0為快速。 預設值:1.0。 取值範圍:[0.5, 2.0]。 千問-TTS-Realtime(參見支援的模型)不支援該參數。 |
|
volume |
int |
否 |
音訊音量。 預設值:50。 取值範圍:[0, 100]。 千問-TTS-Realtime(參見支援的模型)不支援該參數。 |
|
pitchRate |
float |
否 |
合成音訊語調。 預設值:1.0。 取值範圍:[0.5, 2.0]。 千問-TTS-Realtime(參見支援的模型)不支援該參數。 |
|
bitRate |
int |
否 |
指定音訊碼率(kbps)。碼率越大,音質越好,音頻檔案體積越大。僅在音頻格式( 預設值:128。 取值範圍:[6, 510]。 千問-TTS-Realtime(參見支援的模型)不支援該參數。 |
|
instructions |
String |
否 |
設定指令,參見指令控制。 預設值:無預設值,不設定不生效。 長度限制:長度不得超過 1600 Token。 支援語言:僅支援中文和英文。 適用範圍:該功能僅適用於千問3-TTS-Instruct-Flash-Realtime系列模型。 |
|
optimizeInstructions |
boolean |
否 |
是否對 預設值:false。 行為說明:當設定為 true 時,系統將對 適用情境:推薦在追求高品質、精細化語音表達的情境下開啟。 依賴關係:此參數依賴於 適用範圍:該功能僅適用於千問3-TTS-Instruct-Flash-Realtime系列模型。 |
關鍵介面
QwenTtsRealtime類
引入方法:
import com.alibaba.dashscope.audio.qwen_tts_realtime.QwenTtsRealtime;
|
成員方法 |
方法簽名 |
服務端響應事件(通過回調下發) |
說明 |
|
connect |
|
會話已建立 會話配置已更新 |
和服務端建立串連。 |
|
updateSession |
|
會話配置已更新 |
更新本次會話互動的預設配置。參數配置請參考《請求參數》章節。 在您建立連結,服務端會及時返回用於此會話的預設輸出輸入配置。如果您需要更新預設會話配置,我們也推薦您總是在建立連結後即刻調用此介面。 服務端在收到session.update事件後,會進行參數校正,如果參數不合法則返回錯誤,否則更新服務端側的會話配置。 |
|
appendText |
|
無 |
將文本片段追加到雲端輸入文本緩衝區。 緩衝區是你可以寫入並稍後提交的臨時儲存。
|
|
clearAppendedText |
|
清空服務端收到的文本 |
刪除當前雲端緩衝區的文本。 |
|
commit |
|
提交文本並觸發語音合成 響應時有新的輸出內容 新的輸出內容添加到assistant message 項 模型增量產生的音頻 完成音頻產生 Assistant message 的音頻內容流式輸出完成 Assistant message 的整個輸出項串流完成 響應完成 |
提交之前通過append添加到雲端緩衝區的文本,並立刻合成所有文本。如果輸入的文本緩衝區為空白將產生錯誤。
|
|
finish |
|
響應完成 |
終止任務。 |
|
close |
|
無 |
關閉串連。 |
|
getSessionId |
|
無 |
擷取當前任務的session_id。 |
|
getResponseId |
|
無 |
擷取最近一次response的response_id。 |
|
getFirstAudioDelay |
|
無 |
擷取首包音頻延遲。 |
回調介面(QwenTtsRealtimeCallback)
|
方法 |
參數 |
傳回值 |
描述 |
|
無 |
無 |
當和服務端建立串連完成後,該方法立刻被回調。 |
|
message:服務端響應事件。 |
無 |
包括對介面調用的回複響應和模型產生的文本和音頻。具體可以參考:服務端事件 |
|
code:關閉WebSocket的狀態代碼。 reason:關閉WebSocket的關閉資訊。 |
無 |
當服務已經關閉串連後進行回調。 |