实时语音合成 Java SDK 接入指南

更新时间:
复制 MD 格式

本文面向 Java 开发人员,介绍如何使用 ConvAI Voice SDK 接入实时语音合成服务,实现流式文本输入、流式音频接收、音频文件保存、PCM 实时播放、字级时间戳和任务取消。

适用版本:

  • JDK:8 及以上

  • SDK:com.aliyun.convai:convai-voice-sdk:0.1.0(请使用最新版本)

  • 协议:WebSocket(SDK 内部管理,业务无需手工拼装协议帧)

1. 接入前准备

接入前请向服务方获取:

  1. Voice Studio 签发的 API Key,形如 sk-xxxxxxxx。可通过控制台获取创建 API Key。

  2. 平台音色 ID,即 ListConsumableVoices 返回的 voiceVersionId。可通过控制台获取音色ID。

1.1 服务地址

wss://convai-platform.aliyuncs.com/api-ws/v1/inference

1.2 Maven 依赖

在 pom.xml 中加入:

<dependency>
  <groupId>com.aliyun.convai</groupId>
  <artifactId>convai-voice-sdk</artifactId>
  <version>0.1.0</version>
</dependency>

<!-- 可选:SDK 使用 slf4j-api,应用可自行选择日志实现 -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>2.0.6</version>
  <scope>runtime</scope>
</dependency>

2. SDK 核心对象

对象

作用

生命周期建议

VoiceRealtimeClient

管理鉴权、WebSocket 连接、连接池、心跳和重连

重对象,一个应用进程创建一个并全局复用

SynthesisOptions

描述一次合成的音色、格式、采样率等参数

每种合成配置可复用

SynthesisSession

一轮流式合成会话

每轮对话创建一个;单生产者线程调用

SynthesisListener

接收音频分片、句级事件和任务事件

回调内只做快速写盘或入队

SynthesisResult

合成结束结果,包含计费字符、首包延迟和链路 ID

finish() 或 cancel() 返回

推荐调用流程:

创建全局 VoiceRealtimeClient
        ↓
为本轮合成创建 SynthesisSession
        ↓
write(text) 可调用多次,SDK 流式返回音频
        ↓
finish() 等待所有尾部音频发送完毕
        ↓
关闭 session
        ↓
应用退出时关闭 client

3. 快速开始:短文本合成并保存 WAV

以下示例使用 synthesize() 一次性发送短文本,阻塞等待完整音频并保存为可直接播放的 WAV。

import com.convai.voice.sdk.AudioFormat;
import com.convai.voice.sdk.SynthesisAudio;
import com.convai.voice.sdk.SynthesisOptions;
import com.convai.voice.sdk.realtime.VoiceRealtimeClient;

import java.nio.file.Files;
import java.nio.file.Paths;

public class ShortTextWavExample {
    public static void main(String[] args) throws Exception {
        String apiKey = System.getenv("CONVAI_API_KEY");
        String voiceId = System.getenv("CONVAI_VOICE_ID");

        VoiceRealtimeClient client = VoiceRealtimeClient.builder()
                .endpoint("wss://convai-platform.aliyuncs.com/api-ws/v1/inference")
                .apiKey(apiKey)
                .build();
        try {
            SynthesisOptions options = SynthesisOptions.builder()
                    .voice(voiceId)
                    .format(AudioFormat.WAV)
                    .sampleRate(22050)
                    .build();

            SynthesisAudio result = client.synthesize(
                    options, "你好,欢迎使用实时语音合成服务。");
            Files.write(Paths.get("output.wav"), result.audio());

            System.out.println("音频字节数=" + result.size());
            System.out.println("计费字符=" + result.result().characters());
            System.out.println("首包延迟=" + result.result().firstPackageDelayMillis() + " ms");
            System.out.println("requestUuid=" + result.result().lastRequestUuid());
        } finally {
            client.close();
        }
    }
}

运行前设置:

export CONVAI_API_KEY="sk-你的VoiceStudioApiKey"
export CONVAI_VOICE_ID="你的平台音色ID"
mvn -q compile org.codehaus.mojo:exec-maven-plugin:3.1.0:java \
  -Dexec.mainClass=ShortTextWavExample

4. 完整参数列表

4.1 VoiceRealtimeClient.Builder 客户端参数

Builder 方法

必填

默认值

支持状态与约束

endpoint(String)

是

无

实时语音合成 WebSocket 服务地址

apiKey(String)

二选一

无

固定 API Key;请勿写死在代码或提交到代码仓库

apiKeyProvider(ApiKeyProvider)

二选一

无

适合配置中心或临时凭证;鉴权失败时可刷新凭证

textNormalization(boolean)

否

false

开启 TN;握手级配置,同一个 client 的所有会话共享

userAgent(String)

否

convai-voice-sdk-java

建议设置应用名称和版本,方便服务端排障

header(String, String)

否

无

添加自定义握手请求头;不要覆盖鉴权或平台保留头

connectTimeoutMillis(long)

否

10000

WebSocket 建连超时,单位毫秒

taskStartTimeoutMillis(long)

否

10000

等待任务启动确认的超时,单位毫秒

finishTimeoutMillis(long)

否

600000

finish() 等待全部音频完成的上限;长文本建议调大

guards(GuardPolicy)

否

GuardPolicy.defaults()

连接空闲、任务停顿和心跳保护;生产环境建议保持默认

reconnect(ReconnectPolicy)

否

ReconnectPolicy.defaults()

建连失败及并发超限的重试退避策略

resume(ResumePolicy)

否

FAIL_FAST

网络中断后的处理策略,详见第 10 节

默认保护策略:

  • WebSocket 心跳:20 秒。

  • 空闲连接回收:45 秒。

  • 流式文本停顿保护:18 秒;SDK 会在服务端超时前正常收口当前 task。

  • 建连最大尝试次数:5 次,采用随机/指数退避。

4.2 SynthesisOptions.Builder 合成参数

Builder 方法

必填

默认值

支持状态与约束

voice(String)

是

无

平台音色 ID(voiceVersionId),不能传音色名

format(AudioFormat)

否

MP3

MP3 / PCM / WAV / OPUS

sampleRate(int)

否

22050

8000 / 16000 / 22050 / 24000 / 44100 / 48000;OPUS 仅支持 8000 / 16000 / 24000 / 48000

volume(int)

否

50

范围 [0, 100]

rate(double)

否

1.0

语速,范围 [0.5, 2.0]

pitch(double)

否

1.0

音调,范围 [0.5, 2.0]

bitRate(Integer)

否

引擎默认

范围 [6, 510] kbps,仅 MP3/OPUS 支持

seed(Integer)

否

无

范围 [0, 65535];相同参数有助于复现结果

languageHints(String...)

否

空

语言提示,如 "zh";当前仅首元素生效

instruction(String)

否

无

方言、情感或角色指令,最长 2000 字符;具体支持取决于音色绑定模型

enableSsml(boolean)

否

false

开启后一个任务只允许调用一次 write();不能与字级时间戳同时开启

wordTimestampEnabled(boolean)

否

false

在句末事件返回字级时间戳;不能与 flush() 同时使用

hotFix(HotFix)

否

无

自定义发音和文本替换;具体支持取决于音色绑定模型

enableMarkdownFilter(boolean)

否

false

合成前过滤 Markdown 标记;具体支持取决于模型

enableAigcTag(boolean)

否

false

在音频中嵌入 AIGC 标识

aigcPropagator(String)

否

无

需先设置 enableAigcTag(true)

aigcPropagateId(String)

否

无

需先设置 propagator;用于标识传播行为

parameter(String, Object)

否

无

透传 SDK 尚未建模的新参数;不能覆盖 SDK 已管理字段

parameters(Map<String,Object>)

否

无

批量设置额外参数

SDK 不提供 model 参数。模型由平台根据音色 ID 的绑定关系自动确定,客户无需感知或传入模型名。

SDK 的格式默认值是 MP3,本文所有文件保存示例均显式设置为 AudioFormat.WAV,不会生成 MP3。

4.3 音频格式建议

格式

特点

推荐场景

MP3

体积小、兼容性高,可按顺序拼接落盘

通用文件保存、网络传输

PCM

延迟最低;16-bit、小端、单声道,无文件头

实时播放、电话或 RTC 音频管道

WAV

自带文件头,便于直接播放

收完后播放、测试验收

OPUS

压缩率高

带宽受限场景

PCM 下行分片长度不固定,不能假设每个 WebSocket 音频分片都是固定 20 ms。应将其视为连续字节流,按收到顺序写入播放缓冲区。

5. SDK 方法说明

5.1 VoiceRealtimeClient

方法

说明

newSession(options, listener)

创建流式会话;第一次 write() 时惰性建连

synthesize(options, text)

短文本便捷接口,阻塞返回完整音频

close()

关闭全部会话、连接池和后台线程;应用退出时调用

5.2 SynthesisSession

方法

说明

start()

提前建连并启动任务,用于 fail-fast;通常无需调用

write(String)

增量发送文本,可调用多次

write(String, boolean)

发送文本;第二个参数为 true 时紧接着执行 flush

flush()

强制合成当前缓存残句;与字级时间戳互斥

finish()

正常结束并阻塞到所有音频接收完成,返回 SynthesisResult

cancel()

用户打断时停止后续音频并结束会话

close()

若会话未结束,会尽力执行取消并释放资源

文本限制:单次 write() 不超过 20,000 字符,单个上游任务累计不超过 200,000 字符。中文长文本建议按完整句子拆分,并保留句末标点。

6. 场景示例二:LLM 流式文本合成并保存 WAV

import com.convai.voice.sdk.*;
import com.convai.voice.sdk.realtime.*;
import java.io.*;
import java.util.concurrent.atomic.AtomicReference;

public class LlmStreamingWavExample {
    public static void main(String[] args) throws Exception {
        VoiceRealtimeClient client = VoiceRealtimeClient.builder()
                .endpoint("wss://convai-platform.aliyuncs.com/api-ws/v1/inference")
                .apiKey(System.getenv("CONVAI_API_KEY"))
                .finishTimeoutMillis(10 * 60 * 1000L)
                .build();

        SynthesisOptions options = SynthesisOptions.builder()
                .voice(System.getenv("CONVAI_VOICE_ID"))
                .format(AudioFormat.WAV)
                .sampleRate(22050)
                .build();

        final AtomicReference<IOException> writeError = new AtomicReference<IOException>();
        try (final OutputStream out = new BufferedOutputStream(new FileOutputStream("stream.wav"))) {
            SynthesisListener listener = new SynthesisListener() {
                public void onAudio(byte[] audio) {
                    try {
                        out.write(audio);
                    } catch (IOException e) {
                        writeError.compareAndSet(null, e);
                    }
                }
                public void onSentence(SentenceEvent event) {
                    if (event.type() == SentenceEvent.Type.SENTENCE_END) {
                        System.out.println("已合成:" + event.originalText());
                    }
                }
            };

            try (SynthesisSession session = client.newSession(options, listener)) {
                // 实际业务中,这些字符串可直接来自大模型流式输出。
                session.write("今天的天气非常不错,");
                session.write("适合出门散步。");
                session.write("记得带上水杯。");

                SynthesisResult result = session.finish();
                out.flush();
                if (writeError.get() != null) throw writeError.get();
                System.out.println("计费字符=" + result.characters());
                System.out.println("requestUuid=" + result.lastRequestUuid());
            }
        } finally {
            client.close();
        }
    }
}

注意:

  • onAudio 在 WebSocket 读线程执行,只做快速写盘或入队。

  • 不要在 onAudio 中调用 write()、flush()、finish() 或执行网络请求。

  • 文本尽量按完整句子发送;无句末标点的残句可在适当时机调用 flush()。

7. 场景示例三:PCM 边合成、边播放、边保存

下面的完整示例使用 JDK 8 自带 Java Sound 播放 PCM,并同步保存裸 PCM;合成结束后自动补 WAV 头生成 output.wav。PCM 参数必须与请求一致:16-bit、小端、单声道、24000 Hz。

import com.convai.voice.sdk.AudioFormat;
import com.convai.voice.sdk.SynthesisOptions;
import com.convai.voice.sdk.SynthesisResult;
import com.convai.voice.sdk.realtime.SynthesisListener;
import com.convai.voice.sdk.realtime.SynthesisSession;
import com.convai.voice.sdk.realtime.VoiceRealtimeClient;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class PcmStreamingPlaybackExample {
    private static final byte[] END = new byte[0];

    public static void main(String[] args) throws Exception {
        final int sampleRate = 24000;
        final BlockingQueue<byte[]> queue = new LinkedBlockingQueue<byte[]>();

        javax.sound.sampled.AudioFormat javaSoundFormat =
                new javax.sound.sampled.AudioFormat(sampleRate, 16, 1, true, false);
        final SourceDataLine speaker = (SourceDataLine) AudioSystem.getLine(
                new DataLine.Info(SourceDataLine.class, javaSoundFormat));
        speaker.open(javaSoundFormat);
        speaker.start();

        final OutputStream recording = new BufferedOutputStream(
                new FileOutputStream("output.pcm"));
        Thread consumer = new Thread(new Runnable() {
            public void run() {
                try {
                    while (true) {
                        byte[] pcm = queue.take();
                        if (pcm == END) break;
                        recording.write(pcm);
                        speaker.write(pcm, 0, pcm.length);
                    }
                    recording.flush();
                    speaker.drain();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    try { recording.close(); } catch (IOException ignored) { }
                    speaker.stop();
                    speaker.close();
                }
            }
        }, "pcm-playback");
        consumer.start();

        VoiceRealtimeClient client = VoiceRealtimeClient.builder()
                .endpoint("wss://convai-platform.aliyuncs.com/api-ws/v1/inference")
                .apiKey(System.getenv("CONVAI_API_KEY"))
                .build();
        SynthesisOptions options = SynthesisOptions.builder()
                .voice(System.getenv("CONVAI_VOICE_ID"))
                .format(AudioFormat.PCM)
                .sampleRate(sampleRate)
                .build();

        try {
            SynthesisListener listener = new SynthesisListener() {
                public void onAudio(byte[] pcm) {
                    queue.offer(pcm); // 回调只入队,避免扬声器写入阻塞 WS 读线程
                }
            };
            try (SynthesisSession session = client.newSession(options, listener)) {
                session.write("你好,现在正在边合成边播放 PCM 音频。");
                session.write("完整音频也会同步保存到本地文件。");
                SynthesisResult result = session.finish();
                System.out.println("requestUuid=" + result.lastRequestUuid());
            }
        } finally {
            client.close();
            queue.offer(END);
            consumer.join();
        }
        pcmToWav("output.pcm", "output.wav", sampleRate);
        System.out.println("WAV 录音已保存:" + Paths.get("output.wav").toAbsolutePath());
    }

    private static void pcmToWav(String pcmPath, String wavPath, int sampleRate)
            throws Exception {
        byte[] pcm = Files.readAllBytes(Paths.get(pcmPath));
        ByteBuffer header = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN);
        header.put("RIFF".getBytes("US-ASCII"));
        header.putInt(36 + pcm.length);
        header.put("WAVEfmt ".getBytes("US-ASCII"));
        header.putInt(16).putShort((short) 1).putShort((short) 1);
        header.putInt(sampleRate).putInt(sampleRate * 2);
        header.putShort((short) 2).putShort((short) 16);
        header.put("data".getBytes("US-ASCII")).putInt(pcm.length);
        try (OutputStream out = Files.newOutputStream(Paths.get(wavPath))) {
            out.write(header.array());
            out.write(pcm);
        }
    }
}

裸 PCM 播放验证:

ffplay -f s16le -ar 24000 -ac 1 output.pcm

示例结束后会生成可直接播放的 output.wav。生产环境处理超长音频时,建议使用 RandomAccessFile 边写 PCM,并在关闭时回填 WAV 文件头长度,避免一次性把完整 PCM 读入内存。

生产播放建议:

  • 使用独立播放线程和有界抖动缓冲,不要在 SDK 回调线程直接播放。

  • 播放缓冲欠载时输出静音或舒适噪声,不要反复播放最后一个有声帧。

  • 不要按 WebSocket 分片大小推断音频时长;PCM 时长为 总字节数 / 2 / 采样率。

8. 场景示例四:文本归一化与字级时间戳

文本归一化(TN)是 client 级配置;字级时间戳是会话参数。开启 TN 后,时间戳偏移基于归一化后的文本。

import com.convai.voice.sdk.*;
import com.convai.voice.sdk.realtime.*;
import java.io.*;

public class WordTimestampExample {
    public static void main(String[] args) throws Exception {
        VoiceRealtimeClient client = VoiceRealtimeClient.builder()
                .endpoint("wss://convai-platform.aliyuncs.com/api-ws/v1/inference")
                .apiKey(System.getenv("CONVAI_API_KEY"))
                .textNormalization(true)
                .build();
        SynthesisOptions options = SynthesisOptions.builder()
                .voice(System.getenv("CONVAI_VOICE_ID"))
                .format(AudioFormat.WAV)
                .sampleRate(22050)
                .wordTimestampEnabled(true)
                .build();

        try (final OutputStream out = new FileOutputStream("timestamp.wav")) {
            SynthesisListener listener = new SynthesisListener() {
                public void onAudio(byte[] audio) {
                    try { out.write(audio); } catch (IOException e) { throw new RuntimeException(e); }
                }
                public void onSentence(SentenceEvent event) {
                    if (event.type() != SentenceEvent.Type.SENTENCE_END) return;
                    System.out.println("句子:" + event.originalText());
                    for (Word word : event.words()) {
                        System.out.printf("%s: %d-%d ms%n",
                                word.text(), word.beginTime(), word.endTime());
                    }
                }
            };
            try (SynthesisSession session = client.newSession(options, listener)) {
                session.write("今天是2026年9月24日,订单金额是128.5元。");
                session.finish();
            }
        } finally {
            client.close();
        }
    }
}

字级时间戳开启后不能调用 flush() 或 write(text, true);SSML 与字级时间戳也不能同时开启。

9. 场景示例五:用户打断并取消合成

当用户开始说话、切换页面或主动停止播放时,应调用 cancel(),不要直接断开底层 WebSocket。

import com.convai.voice.sdk.*;
import com.convai.voice.sdk.realtime.*;

public class CancelSynthesisExample {
    public static void main(String[] args) throws Exception {
        VoiceRealtimeClient client = VoiceRealtimeClient.builder()
                .endpoint("wss://convai-platform.aliyuncs.com/api-ws/v1/inference")
                .apiKey(System.getenv("CONVAI_API_KEY"))
                .build();
        SynthesisOptions options = SynthesisOptions.builder()
                .voice(System.getenv("CONVAI_VOICE_ID"))
                .format(AudioFormat.PCM)
                .sampleRate(24000)
                .build();

        try {
            SynthesisListener listener = new SynthesisListener() {
                public void onAudio(byte[] audio) {
                    // 实际业务中把音频放入播放器队列。
                    System.out.println("收到 PCM: " + audio.length + " bytes");
                }
            };
            try (SynthesisSession session = client.newSession(options, listener)) {
                session.write("这是一段较长的合成文本,用于演示用户主动打断。"
                        + "用户开始说话后,客户端应立即取消后续合成。");

                Thread.sleep(500); // 模拟用户在播放过程中触发打断
                SynthesisResult result = session.cancel();
                System.out.println("已取消,cancelled=" + result.cancelled());
            }
        } finally {
            client.close();
        }
    }
}

取消会丢弃尚未合成的缓存文本,并停止后续音频输出;已输出的音频无法撤回。取消不等同于“不产生用量”。

10. 错误处理与重试

SDK 异常均继承 VoiceSdkException,属于非受检异常。

异常

含义

建议处理

InvalidOptionException

本地参数校验失败

修改参数,不要重试

AuthFailedException

API Key 无效、过期或音色越权

刷新凭证或检查音色权限

ConcurrencyLimitException

并发连接数超限,SDK 退避后仍失败

降低并发或稍后重试

TaskFailedException

服务端返回任务失败

读取错误码判断是否可重试

TaskInterruptedException

网络异常导致合成中途被打断

根据已出声边界决定是否补发

TransportException

建连、发送或等待超时

检查网络并进行有限次数重试

推荐错误处理:

try {
    SynthesisResult result = session.finish();
} catch (com.convai.voice.sdk.error.TaskFailedException e) {
    System.err.println("errorCode=" + e.rawErrorCode());
    System.err.println("taskId=" + e.taskId());
    System.err.println("requestUuid=" + e.requestUuid());
    System.err.println("sessionId=" + e.sessionId());
    if (e.errorCode().retryable()) {
        // 由业务进行有上限、带退避的重试。
    }
} catch (com.convai.voice.sdk.error.TaskInterruptedException e) {
    System.err.println("已完成合成句数=" + e.spokenSentences());
    System.err.println("待续传文本=" + e.unspokenText());
}

这里的“已完成合成句数”表示 SDK 已收到相应句子的完成事件,不代表音频已经通过扬声器播放。实时播放场景应等待播放队列消费完毕,并调用 SourceDataLine.drain() 后,才能确认音频播放完成。

ResumePolicy:

  • FAIL_FAST(默认):网络中断时抛出 TaskInterruptedException,由业务决定是否重放。

  • RESUME_UNSPOKEN:SDK 自动重发未确认出声的文本。可能出现少量重复、缺失或韵律变化,开启 TN 时边界估算也可能漂移,仅适合“连续性优先于听感一致性”的场景。

11. 结果与可观测信息

SynthesisResult 提供:

方法

说明

characters()

本次会话累计计费字符数

segments()

SDK 自动切段后的各段结果

cancelled()

是否由取消结束

firstPackageDelayMillis()

第一段从首帧文本发送到首帧音频到达的延迟

lastRequestUuid()

最后一段服务端链路 ID,报障时必须提供

每个 Segment 提供:taskId()、requestUuid()、sessionId()、characters()、autoSegmented() 和 firstPackageDelayMillis()。

SDK 为避免任务内文本停顿超时,可能将一个业务会话自动拆成多个上游 task。因此:

  • segments().size() 可能大于 1,属于正常行为。

  • 句子 index 在每个 task 内从 0 开始,应结合 SentenceEvent.taskId() 区分。

  • 报障时建议提交全部 segment 的 taskId、requestUuid 和 sessionId。

12. 连接与并发最佳实践

  1. 一个应用进程只创建一个 VoiceRealtimeClient,不要每次合成都重新创建。

  2. 每轮对话新建一个 SynthesisSession,不要跨线程并发调用同一个 session。

  3. 正常结束必须调用 finish();用户打断调用 cancel()。

  4. 必须等 finish() 返回后再关闭录音文件,否则会丢失尾部音频。

  5. 回调线程只做快速写盘或入队;播放、转码、上传等耗时操作放到独立线程。

  6. 并发会话会占用并发连接额度;出现并发超限时不要密集重连。

  7. 心跳只能维持网络链路,不代表业务连接永久有效;连接生命周期由 SDK 自动管理。

  8. API Key 不得写入客户端安装包、浏览器代码、日志或代码仓库,应通过服务端配置或密钥系统注入。

13. 常见问题

13.1 为什么没有立即出声?

引擎通常按句合成。请保留句末标点,或在允许的场景调用 flush()。频繁 flush 会让语音断句变碎,不建议对每个 token 调用。

13.2 为什么音频尾部缺失?

必须调用 finish() 并等待其返回。finish() 返回前仍可能持续收到音频分片。

13.3 为什么音色名返回不存在?

SDK 的 voice() 只接受平台音色 ID,即 voiceVersionId,不接受供应商音色名。

13.4 PCM 播放出现变速、变调或噪音怎么办?

确认播放器参数与请求完全一致:

  • 16-bit signed PCM

  • little-endian

  • mono

  • 采样率等于 sampleRate()

不要将 PCM 当成 8-bit、big-endian、双声道或错误采样率播放。

13.5 为什么收到多个 taskId?

当 LLM 输出长时间停顿时,SDK 会在服务端超时前自动正常收口当前 task,下一次 write() 自动创建新 task。业务会话仍是同一个,最终结果通过 segments() 汇总。

13.6 发生问题需要提供什么?

请提供:

  1. 问题发生的准确时间。

  2. requestUuid、taskId、sessionId。

  3. SDK 版本和 JDK 版本。

  4. 音频格式、采样率和音色 ID。

  5. 完整异常类型、错误码及错误消息。

  6. 若为音频问题,请提供 SDK 原始落盘文件,不要只提供经过播放器、RTC 或电话系统转码后的录音。