本文介紹 DashScope Java SDK 調用 即時語音合成 時的關鍵介面與請求參數。
使用者指南:關於模型介紹和選型建議請參見即時語音合成或非即時語音合成。
前期準備
DashScope Java SDK 版本需要不低於2.22.7。
快速開始
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);
}
}
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 | 否 | |
sampleRate | int | 否 | |
speechRate | float | 否 | |
volume | int | 否 | |
pitchRate | float | 否 | |
bitRate | int | 否 | |
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 | |
| 提交之前通過append添加到雲端緩衝區的文本,並立刻合成所有文本。如果輸入的文本緩衝區為空白將產生錯誤。
|
finish | |
| 終止任務。 |
close | | 無 | 關閉串連。 |
getSessionId | | 無 | 擷取當前任務的session_id。 |
getResponseId | | 無 | 擷取最近一次response的response_id。 |
getFirstAudioDelay | | 無 | 擷取首包音頻延遲。 |
回調介面(QwenTtsRealtimeCallback)
方法 | 參數 | 傳回值 | 描述 |
|---|---|---|---|
| 無 | 無 | 當和服務端建立串連完成後,該方法立刻被回調。 |
| message:服務端響應事件。 | 無 | 包括對介面調用的回複響應和模型產生的文本和音頻。具體可以參考:服務端事件 |
| code:關閉WebSocket的狀態代碼。 reason:關閉WebSocket的關閉資訊。 | 無 | 當服務已經關閉串連後進行回調。 |