通過 AOQ 接入 qwen-audio-3.0-realtime-plus,使用服務端 VAD 自動劃分輪次,實現低延遲的即時語音對話。用戶端代碼以 Android Java 為例。
方案概述
Qwen-Audio 是端到端即時語音互動模型,適用於語音助手、智能客服和 AI 伴侶等需要低延遲語音互動的情境。AOQ SDK 將音頻與事件分軌傳輸:Audio 軌負責上行麥克風 PCM 和下行模型 PCM,Data 軌負責 Realtime 協議事件。
本教程使用 server_vad:用戶端持續上行音頻,服務端自動識別使用者開始和停止說話,並觸發模型回複。
準備工作
- 開通阿里雲百鍊,並按擷取與配置 API Key。API Key 只儲存在業務 AppServer,不要寫入用戶端代碼或提交到代碼倉庫。
- 根據業務部署地區確認 AOQ Endpoint。地區和接入地址的選擇方法請參見選擇地區、服務部署範圍和接入網域名稱。
- 從SDK 下載擷取最新版 AOQ Client SDK。
- 搭建業務 AppServer,並按Token 鑒權實現服務端代理鑒權。每次建立新串連前,用戶端都應從 AppServer 擷取新的串連憑證。
匯入 SDK
根據開發平台匯入對應 SDK。後續用戶端代碼以 Android Java 為例,其他平台使用相同的介面設計和事件流程。本文以 PCM 音頻流為例。Opus 編碼由外掛程式提供;如果需要使用 Opus 編碼上行,請匯入 Opus 外掛程式。
Android
- 將 AoqClientSdk-release.aar 放入 app/libs,並在 app/build.gradle 中配置依賴和 SDK 支援的 ABI:
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
- 在 AndroidManifest.xml 中聲明以下許可權:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
- 在使用相應裝置前動態申請 RECORD_AUDIO 許可權。
iOS
- 將 AoqClientSdk.framework 拖入 Xcode 工程,在 Target > General > Frameworks, Libraries, and Embedded Content 中選擇 Embed & Sign。SDK 支援 iOS 13.0 及以上 arm64 裝置。
- 在 Info.plist 中添加 NSMicrophoneUsageDescription,並在使用相應裝置前請求使用者授權。
- Swift 工程使用 import AoqClientSdk;Objective-C 工程使用 #import <AoqClientSdk/AoqClientSdk.h>。
HarmonyOS
- 將 AoqClientSdk.har 放入 entry/libs,並在 entry/oh-package.json5 中聲明依賴。該 SDK 相容 API 12,支援 arm64-v8a:
{
"dependencies": {
"@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
}
}
- 在 entry/src/main/module.json5 中聲明以下許可權:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.MICROPHONE",
"reason": "$string:perm_mic_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }
]
- 在使用相應裝置前調用 abilityAccessCtrl.createAtManager().requestPermissionsFromUser 申請 ohos.permission.MICROPHONE。
Linux (Python)
- 解壓 SDK,並保持 aoq_client_sdk.py、libAoqClientSdk.so 和 libonnxruntime.so.1.16.3 位於同一目錄。
- 將 SDK 目錄加入 Python 和動態庫搜尋路徑:
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
- 在 Python 代碼中使用 import aoq_client_sdk。也可通過 AOQ_CLIENT_SDK_LIB 指定 libAoqClientSdk.so 的絕對路徑。
體驗 Demo
阿里雲百鍊提供適用於 Android 平台的 Demo,可用於快速驗證 AOQ 接入效果。下載 APK 並配置 API Key 和 workspaceId 後,即可體驗部分模型。
掃描以下二維碼下載 Demo:
實現流程
- AppServer 通過 Realtime Token 地址擷取 qwen-audio-3.0-realtime-plus 的本次 AOQ 串連憑證。
- 用戶端根據接入模型和業務音頻格式配置 SDK 的上行編碼與下行解碼參數。
- 用戶端初始化錄音和播放裝置,建立 AoqConnectConfig,將本次串連憑證寫入對應欄位,並配置需要發布和訂閱的 Audio、Data 軌。保持 Audio 軌發送關閉,調用 connect 建立 AOQ 串連。
- 串連成功後發送 session.update;收到 session.updated 後才開啟 Audio 軌發送。
- 服務端 VAD 自動劃分輪次,模型音頻通過 Audio 軌自動播放,Data 軌持續返回對話事件。
- 結束使用時中斷連線並銷毀引擎,SDK 會自動關閉音訊裝置。
AppServer 擷取 Token
在 AppServer 設定 DASHSCOPE_API_KEY,並使用所選地區的 Endpoint 發送請求。clientIp 為用戶端的真實公網 IP;該欄位可選,但建議傳入,以便服務分配合適的 Relay 存取點。
curl -X POST \
"https://{endpoint}/api/v1/webrtc/realtime?model=qwen-audio-3.0-realtime-plus" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
-H "x-dashscope-rtc-transport: moq" \
-d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"
說明如果 AppServer 無法擷取用戶端真實公網 IP,請刪除 clientIp 欄位,不要傳Null 字元串。
AppServer 將響應中的以下欄位返回用戶端。AOQ Token 僅供一次串連使用,用戶端每次調用 connect 前都必須重新請求,不能緩衝或複用。生產環境中不要把 API Key 返回用戶端。完整請求和響應欄位請參見Token 鑒權。
響應欄位 | SDK 欄位 |
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
實現 Android 用戶端
每次建立串連前,用戶端先從 AppServer 擷取新的串連憑證,再建立 AoqConnectConfig:映射 Token 響應欄位,並補充發布和訂閱軌道等用戶端串連參數。按以下步驟實現 Android 端即時語音對話。
1. 建立引擎並設定回調
建立 AOQ 單例引擎並註冊事件回調。客戶需要在串連成功時配置會話,並把服務端事件分發給 UI 和業務狀態機器。
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
configureSession();
}
}
@Override
public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
handleServerEvent(msg);
}
};
AoqClientEngine.AoqCreateConfig createConfig = new AoqClientEngine.AoqCreateConfig();
createConfig.workDir = context.getFilesDir().getAbsolutePath();
engine = AoqClientEngine.createEngine(context, createConfig, listener);
2. 配置音頻編解碼
根據接入模型和業務音頻格式配置 SDK 的上行編碼與下行解碼參數。以下數值僅為本教程的 PCM 樣本配置,不限制客戶的業務音頻格式。
AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioEncoderConfig.sampleRate = 16000; // 樣本值,請按接入模型和業務音頻格式調整。
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioDecoderConfig.sampleRate = 24000; // 樣本值,應與模型下行音頻格式一致。
audioDecoderConfig.channel = 1;
engine.setAudioDecoderConfig(audioDecoderConfig);
3. 配置軌道並建立串連
使用 SDK 介面啟動音頻採集與播放。將本次 AppServer Token 響應映射到 AoqConnectConfig 的憑證欄位,並在 publishTracks 和 subscribeTracks 中分別配置 Audio 和 Data 軌。保持 Audio 軌發送關閉,調用 connect 建立串連;後續收到 session.updated 後再開啟發送。
AoqClientEngine.AoqAudioCaptureConfig captureConfig =
new AoqClientEngine.AoqAudioCaptureConfig();
captureConfig.channel = 1;
captureConfig.isVoipMode = true;
engine.startAudioCapture(captureConfig);
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isVoipMode = true;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
AoqClientEngine.AoqTrackParam publishAudioTrack = new AoqClientEngine.AoqTrackParam();
publishAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.publishTracks.add(publishAudioTrack);
AoqClientEngine.AoqTrackParam publishDataTrack = new AoqClientEngine.AoqTrackParam();
publishDataTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeData;
connectConfig.publishTracks.add(publishDataTrack);
AoqClientEngine.AoqTrackParam subscribeAudioTrack = new AoqClientEngine.AoqTrackParam();
subscribeAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.subscribeTracks.add(subscribeAudioTrack);
AoqClientEngine.AoqTrackParam subscribeDataTrack = new AoqClientEngine.AoqTrackParam();
subscribeDataTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeData;
connectConfig.subscribeTracks.add(subscribeDataTrack);
engine.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);
4. 發送 session.update
串連成功後配置輸出模態、音色、音頻格式、系統指令和 VAD。input_audio_format 與 output_audio_format 的值均為 pcm;實際採樣率由 SDK 編解碼配置確定。完整參數請參見用戶端事件。
JSONObject vad = new JSONObject()
.put("type", "server_vad")
.put("threshold", 0.5)
.put("silence_duration_ms", 800);
JSONObject session = new JSONObject()
.put("modalities", new JSONArray().put("text").put("audio"))
.put("voice", "longanqian")
.put("input_audio_format", "pcm")
.put("output_audio_format", "pcm")
.put("instructions", "You are a helpful voice assistant.")
.put("turn_detection", vad);
JSONObject sessionUpdate = new JSONObject()
.put("type", "session.update")
.put("session", session);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = sessionUpdate.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
5. 收到 session.updated 後開啟上行
收到 session.updated 表示會話配置已生效,此時才開啟 Audio 軌發送。客戶需要保證此前採集的音頻不會提前進入模型輸入。
if ("session.updated".equals(type)) {
engine.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
}
6. 處理服務端事件
在 onDataMsg 中按 type 展示使用者和模型文本,並處理錯誤。完整事件欄位請參見服務端事件。
if ("response.audio_transcript.delta".equals(type)) {
String delta = event.optString("delta");
// 將模型回複的文本增量展示在 UI 中。
} else if ("conversation.item.input_audio_transcription.completed".equals(type)) {
String transcript = event.optString("transcript");
// 將使用者語音的最終轉寫結果展示在 UI 中。
} else if ("error".equals(type)) {
// 讀取錯誤欄位並更新應用狀態。
}
7. 中斷連線並銷毀引擎
結束通話時中斷連線並銷毀單例引擎。disconnect 或 destroy 會自動關閉音頻採集與播放,無需額外調用停止裝置的介面。
engine.disconnect();
AoqClientEngine.destroy();
主要服務端事件
Data 軌事件以 type 標識,用戶端需要處理以下關鍵事件。完整事件結構請參見服務端事件。
事件 | 說明 |
session.created | 會話已建立並返回預設配置 |
session.updated | 用戶端配置已生效,可以開啟音頻上行 |
input_audio_buffer.speech_started | 檢測到使用者開始說話 |
input_audio_buffer.speech_stopped | 檢測到使用者停止說話 |
input_audio_buffer.committed | 本輪音頻已提交 |
response.created | 模型開始產生回複 |
response.audio_transcript.delta | 模型回複文本增量 |
conversation.item.input_audio_transcription.completed | 使用者語音轉寫完成 |
response.done | 本輪迴複結束 |
error | 服務端錯誤 |
完整樣本
以下類接收已填入本次 AppServer 串連憑證的 AoqConnectConfig,並在類內補充音訊裝置和發布、訂閱軌道配置。每次重新串連都必須擷取新的憑證並建立串連配置。生產代碼還需補充許可權、UI 狀態和串連重試。
import android.content.Context;
import com.alibaba.aoq.clientsdk.AoqClientEngine;
import com.alibaba.aoq.clientsdk.AoqClientListener;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
public final class RealtimeVoiceChatClient {
private AoqClientEngine engine;
public RealtimeVoiceChatClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig) {
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
configureSession();
}
}
@Override
public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
try {
JSONObject event = new JSONObject(
new String(msg.data, StandardCharsets.UTF_8));
String type = event.optString("type");
if ("session.updated".equals(type)) {
engine.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
} else if ("response.audio_transcript.delta".equals(type)) {
String delta = event.optString("delta");
// 將 delta 展示在 UI 中。
} else if ("conversation.item.input_audio_transcription.completed".equals(type)) {
String transcript = event.optString("transcript");
// 將 transcript 展示在 UI 中。
} else if ("error".equals(type)) {
// 讀取錯誤欄位並更新應用狀態。
}
} catch (JSONException e) {
throw new IllegalArgumentException("Invalid server event", e);
}
}
};
AoqClientEngine.AoqCreateConfig createConfig = new AoqClientEngine.AoqCreateConfig();
createConfig.workDir = context.getFilesDir().getAbsolutePath();
engine = AoqClientEngine.createEngine(context, createConfig, listener);
// 樣本參數,請按接入模型和業務音頻格式調整。
AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioEncoderConfig.sampleRate = 16000;
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioDecoderConfig.sampleRate = 24000;
audioDecoderConfig.channel = 1;
engine.setAudioDecoderConfig(audioDecoderConfig);
AoqClientEngine.AoqAudioCaptureConfig captureConfig =
new AoqClientEngine.AoqAudioCaptureConfig();
captureConfig.channel = 1;
captureConfig.isVoipMode = true;
engine.startAudioCapture(captureConfig);
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isVoipMode = true;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
AoqClientEngine.AoqTrackParam publishAudioTrack =
new AoqClientEngine.AoqTrackParam();
publishAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.publishTracks.add(publishAudioTrack);
AoqClientEngine.AoqTrackParam publishDataTrack =
new AoqClientEngine.AoqTrackParam();
publishDataTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeData;
connectConfig.publishTracks.add(publishDataTrack);
AoqClientEngine.AoqTrackParam subscribeAudioTrack =
new AoqClientEngine.AoqTrackParam();
subscribeAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.subscribeTracks.add(subscribeAudioTrack);
AoqClientEngine.AoqTrackParam subscribeDataTrack =
new AoqClientEngine.AoqTrackParam();
subscribeDataTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeData;
connectConfig.subscribeTracks.add(subscribeDataTrack);
engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);
}
private void configureSession() {
try {
JSONObject vad = new JSONObject()
.put("type", "server_vad")
.put("threshold", 0.5)
.put("silence_duration_ms", 800);
JSONObject session = new JSONObject()
.put("modalities", new JSONArray().put("text").put("audio"))
.put("voice", "longanqian")
.put("input_audio_format", "pcm")
.put("output_audio_format", "pcm")
.put("turn_detection", vad);
JSONObject sessionUpdate = new JSONObject()
.put("type", "session.update")
.put("session", session);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = sessionUpdate.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Failed to create session.update", e);
}
}
public void close() {
engine.disconnect();
AoqClientEngine.destroy();
}
}
運行並驗證
- 收到 session.updated 後才開始上行麥克風音頻。
- 使用者停止說話後,服務端自動認可音頻並開始回複;文本事件與 Audio 軌音頻連續返回。
典型情境
切換互動模式
server_vad 適合按靜音時間長度判停;smart_turn 結合聲學與語義判斷輪次;按鍵模式將 turn_detection 設為 null。turn_detection 只能在首次發送音頻前修改,切換模式時應重建立立會話。
切換音色
在首次 session.update 中設定 session.voice。不同模型支援的系統音色可能不同;支援的音色與聲音複刻用法請參見即時語音對話(Qwen-Audio-Realtime)。
擴音器或耳機
通過 AoqAudioPlaybackConfig.isDefaultSpeaker 設定預設輸出裝置,運行中可調用 enableSpeakerphone 切換。
Android 後台通話
Android 10 及以上版本如需在後台繼續採集和播放,應使用 foregroundServiceType="microphone|mediaPlayback" 的前台服務,並在應用仍對使用者可見時啟動。
常見問題
問題 | 處理方法 |
串連失敗 | 確認 Token 未到期、Endpoint 與部署地區一致,並檢查 AoqConnectConfig 欄位對應。 |
會話已建立但無回複 | 確認收到 session.updated 後已開啟 Audio 軌,並檢查 SDK 上行編碼配置是否與模型和業務音頻格式一致。 |
回複沒有聲音 | 確認已訂閱 Audio 軌並啟動音頻播放器,然後檢查 SDK 下行解碼配置是否與模型輸出音頻格式一致。 |
相關文檔
如需查看完整參數、事件欄位或其他平台介面,請參見: