This tutorial describes how to use the AOQ SDK with qwen-audio-3.1-asr-flash-streaming to implement voice input.
Solution overview
A typical voice-input interaction starts when the user clicks the voice-input button and begins speaking. Recognition results appear in the input field in real time. The client implementation has the following key points:
- Capture immediately: Start capturing audio and buffering PCM data as soon as the user clicks the button, while asynchronously requesting connection credentials and establishing the connection. Because connection setup takes time and the user may already be speaking, buffer the audio first and send it afterward.
- Catch up at double speed: After the connection succeeds and the model-ready event arrives, send the buffered audio to the model at double speed. Switch to real-time transmission after catching up.
- Transmit audio over the Data channel: qwen-audio-3.1-asr-flash-streaming uses the Inference protocol (run-task / continue-task / finish-task). This scenario requires buffering audio before sending it at double speed, so use
sendDataMsgto transmit audio as JSON messages. The AOQ server supports the JSON audio message format and automatically parses the messages into binary data to forward to the model. - Interim and final results: The model continuously returns recognition results. Display interim results (
sentence_end: false) in real time and append final results (sentence_end: true) to the input field.
Prerequisites
Before you begin
- Activate Alibaba Cloud Model Studio and obtain an API key as described in Obtain and configure an API key. Store the API key only on your AppServer. Do not embed it in client code or commit it to a code repository.
- Set
{endpoint}to{WorkspaceId}.cn-beijing.maas.aliyuncs.comfor China (Beijing) or{WorkspaceId}.ap-southeast-1.maas.aliyuncs.comfor Singapore. Replace the placeholder with your workspace ID and use the API key for the corresponding region and workspace. - Download the latest AOQ Client SDK from SDK download.
- Set up an AppServer and implement server-side proxy authentication as described in Token authentication. Before each new connection, the client must obtain new connection credentials from the AppServer.
Import the SDK
Download the latest AOQ Client SDK from SDK download and import the SDK for your development platform. The following client examples use Android. Other platforms use the same interface design and event flow.
- Android
- iOS
- HarmonyOS
- Linux (Python)
-
Place
libPluginOpus.sofrom the Opus plugin inapp/src/main/jniLibs/armeabi-v7a/andapp/src/main/jniLibs/arm64-v8a/based on the ABI. PlaceAoqClientSdk-release.aarinapp/libs, and configure the dependencies and supported ABIs inapp/build.gradle: -
Declare the following permissions in
AndroidManifest.xml: -
Request the
RECORD_AUDIOpermission at runtime before using the microphone.
Obtain a token on the AppServer
The client obtains connection credentials through the AppServer. Implement server-side proxy authentication as described in Token authentication. The AppServer calls the Model Studio allocation API and returns the token, SID, relay endpoints, and other information required for the SDK connection.
| Response field | SDK field |
|---|---|
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
Client implementation

1. Create the engine and register callbacks
Create an AoqClientEngine instance and register connection-status and Data-message callbacks. Send run-task after the connection succeeds. The Data-message callback handles server events.
2. Configure audio encoding and start capture
Configure PCM encoding (16 kHz, mono), register a capture callback with setAudioFrameObserver to obtain PCM data, and call enableSendMediaStream(Audio, false) to disable outgoing audio media (audio is sent over the Data channel). Finally, call startAudioCapture() to start capture.
This example uses the AOQ SDK's built-in capture and obtains PCM data through onCapturedAudioFrame. You can also implement your own audio capture, such as Android AudioRecord. Add the captured PCM data to the buffer queue; the subsequent transmission logic is identical.
- Approach A: Built-in SDK capture
- Approach B: Custom capture
The example calls
startAudioCapture, obtains PCM data through onCapturedAudioFrame, and copies it into the application buffer. Register the raw-capture callback for AoqAudioSourceCaptured to buffer audio before the connection is established.3. Configure connection credentials and tracks, and connect
Populate AoqConnectConfig with the credentials returned by the AppServer. Publish Audio + Data tracks and subscribe to the Data track. The Audio track declaration is required for the connection, but outgoing audio media is disabled. Actual audio is transmitted over the Data channel.
An Audio track must be published: The ASR model requires an Audio track declaration when connecting; otherwise, the connection fails. Audio data is not sent over the audio media channel, but over the Data channel in
input_audio_buffer.append messages. Call enableSendMediaStream(Audio, false) before connecting to disable outgoing audio media and prevent the SDK from automatically sending captured audio.4. Send run-task to start recognition
After the connection succeeds, generate a new task_id for each voice-input round, for example with UUID.randomUUID().toString(). In the connection-success callback, use sendDataMsg to send the following JSON. Send run-task only once per round.
The run-task event has the following structure. The task ID is an example value; replace it with the newly generated ID for the current round. For event details, see Client events.
sendDataMsg interface:
5. Send buffered audio at double speed after task-started
Parse JSON in onDataMsg. Switch transmission to the ready state only after receiving the following event for the current task.
task_id match the current task ID. A duplicate task-started must not start another transmission loop.
Audio message format: The Inference protocol for qwen-audio-3.1-asr-flash-streaming (Client events) defines only three control events: run-task, continue-task, and finish-task. Audio itself is not sent as a JSON event. However, transmission over the AOQ Data channel requires audio to be sent as JSON messages. For this purpose, AOQ wraps audio data in an input_audio_buffer.append event at the SDK layer:
| Field | Type | Description |
|---|---|---|
type | string | Fixed value: input_audio_buffer.append |
event_id | string | Unique event identifier for correlation and deduplication |
audio | string | Base64-encoded PCM audio data |
sendDataMsg. The AOQ server decodes the audio field into binary PCM data and forwards it to the ASR model according to the model protocol. The client does not need to handle binary-frame encapsulation. Ensure that the PCM format (sample rate, channel count, and bit depth) matches format and sample_rate declared in run-task.
input_audio_buffer.append JSON message, and send it through sendDataMsg:
onDataMsg callback. Start sending audio on task-started and parse recognition results on result-generated. For event details, see Server events.
6. Handle and display recognition results
The result-generated event carries a recognized sentence with fields such as text and sentence_end. Display interim results in real time and append final results to the input field. For event definitions, see Server events.
7. Send finish-task to end recognition
When the user finishes input, first call engine.stopAudioCapture() to stop capture. Wait until the remaining PCM data in the buffer is drained, then send finish-task to notify the server that all audio has been sent. For event definitions, see Client events.
The
task_id must match the run-task for the current round.sendDataMsg:
8. Disconnect and destroy the engine
After receiving task-finished, release resources as follows if you no longer need to reuse the connection. Also release resources in an error or timeout scenario.
Usage notes
- Wait for
task-startedbefore sending audio; otherwise, the data is discarded. - Send
finish-taskonly after the buffer is drained (queue.size() == 0) to ensure that the model processes the last audio segment. - Do not disconnect immediately after
task-finished. You can reuse the connection for a new task with a newtask_id. - Ignore heartbeat packets (
heartbeat: true) inresult-generated. Do not treat them as recognition results.
Common scenarios
Recognize multiple rounds over one connection
The connection remains available after task-finished. The client can send run-task again with a new task_id to start another round without reconnecting. This is suitable for continuous voice input and reduces repeated connection latency.
Implement custom audio capture
This example uses the AOQ SDK's built-in capture (startAudioCapture + onCapturedAudioFrame). You can also implement your own audio capture. Add captured PCM data (16 kHz, mono, 16-bit) to PcmQueue. The subsequent buffered catch-up and transmission logic is identical.