音频编解码库
音频编解码库(AudioCodec)是 btsmart 应用中用于音频翻译链路的统一编解码抽象层,通过 AudioCodec 抽象基类与 OnAudioStreamCallback 回调契约,将 PCM、OPUS、JL AV2 等不同音频格式的解码能力封装为一致的状态机接口,供 AI 翻译等上层业务透明调用。
Purpose and Scope
本文档介绍 com.jieli.btsmart.tool.translate.codec 包下音频编解码库的完整设计:抽象基类 AudioCodec 的契约定义、回调接口 OnAudioStreamCallback 的事件模型、三个具体编解码器(PcmCodec、OpusCodec、JLAV2Codec)的实现差异、生命周期与状态流转,以及上层业务(如 AI 翻译)的接入方式。
本页范围限定在编解码器抽象层本身。以下相关主题由兄弟页面承载,不在本页展开:
- 音频数据的采集与上传(麦克风采集、音频流上传)参见音频处理相关页面
- 播放环节由
AudioPlayer/TranslationSessionPlayer负责,参见播放器相关页面 - 底层蓝牙协议与
AudioData的传输定义位于com.jieli.bluetoothSDK 中,本页仅引用其类型 - 底层 OPUS 原生解码能力由独立库
com.jieli.jl_audio_decode(OpusManager)提供,本页只描述其在OpusCodec中的封装方式
Overview
为什么需要统一的编解码抽象
在 AI 翻译(AITranslationImpl)场景中,设备端与 App 端需要交换多种音频格式的语音数据:PCM 原始采样数据、OPUS 压缩流、JL 私有 AV2 编码流。若上层业务直接依赖每一种格式的具体实现,将导致:
- 业务代码与格式耦合,新增格式需要改动所有调用方;
- 解码器的启动、停止、异常处理逻辑无法复用;
- 无法在运行时按需切换编码格式。
AudioCodec 抽象基类正是为了解决这一问题而设计:它把"解码流"建模为 启动 → 写入 → 回调输出 → 停止 的状态机,并向上层暴露 @AudioType 标注的音频类型标识。上层只需持有一个 AudioCodec 引用,即可对任意格式执行相同的操作序列,而格式差异被封装在子类内部。
核心概念与术语
| 术语 | 说明 |
|---|---|
AudioCodec | 编解码器抽象基类,定义解码流生命周期契约 |
OnAudioStreamCallback | 解码流事件回调接口,向业务层上报 start/stop/error/stream 事件 |
AudioData | 蓝牙 SDK(com.jieli.bluetooth.bean.translation)中的音频数据载体,携带类型与字节负载 |
@AudioType | 蓝牙 SDK 注解,标注音频类型常量(如 Constants.AUDIO_TYPE_PCM、AUDIO_TYPE_OPUS) |
OpusManager | 独立库 com.jieli.jl_audio_decode 提供的 OPUS 原生编解码管理器 |
OpusOption | OPUS 解码器启动参数对象 |
使用场景
- AI 翻译(
AITranslationImpl):接收设备端上传的音频流,按类型选择对应AudioCodec解码,再送入语音识别/合成链路; - 文件转码:
OpusCodec.encodeFile()提供 PCM 文件 → OPUS 文件的离线编码能力,用于语音数据的预处理或回放准备; - 格式扩展:未来新增音频格式时,仅需新增一个
AudioCodec子类,无需改动上层业务。
Architecture
下图展示编解码库的整体架构与依赖关系:
flowchart TD
subgraph sg_Business["业务层 (tool/translate)"]
AITranslationImpl["AITranslationImpl"]
CustomCallback["CustomAudioStreamCallback<br/>(implements OnAudioStreamCallback)"]
end
subgraph sg_CodecLib["编解码库 (tool/translate/codec)"]
AudioCodec["AudioCodec<br/>(abstract base)"]
PcmCodec["PcmCodec"]
OpusCodec["OpusCodec"]
JLAV2Codec["JLAV2Codec"]
Callback["OnAudioStreamCallback<br/>(interface)"]
end
subgraph sg_Native["底层依赖"]
OpusManager["OpusManager<br/>(jl_audio_decode)"]
BluetoothSDK["蓝牙 SDK<br/>(AudioData / Constants)"]
end
subgraph sg_Player["播放侧"]
AudioPlayer["AudioPlayer"]
SessionPlayer["TranslationSessionPlayer"]
end
AITranslationImpl -->|"持有并使用"| AudioCodec
AITranslationImpl --> CustomCallback
CustomCallback -.->|"implements"| Callback
AudioCodec --> Callback
PcmCodec -->|"extends"| AudioCodec
OpusCodec -->|"extends"| AudioCodec
JLAV2Codec -->|"extends"| AudioCodec
OpusCodec -->|"封装调用"| OpusManager
OpusCodec -->|"类型常量"| BluetoothSDK
PcmCodec -->|"类型常量"| BluetoothSDK
AudioCodec -->|"数据载体"| BluetoothSDK
AITranslationImpl -->|"解码输出"| AudioPlayer
AudioPlayer --> SessionPlayer
架构解读:
- 抽象层(契约):
AudioCodec定义了所有编解码器必须实现的生命周期方法,并持有lastAudioData(上一包音频数据,可用于调试与重放);OnAudioStreamCallback定义了解码事件的四类回调。 - 实现层(策略):
PcmCodec、OpusCodec、JLAV2Codec是三种格式的具体策略。PcmCodec是纯转发实现(PCM 无需解码,直接透传),OpusCodec委托原生OpusManager完成实际解码,JLAV2Codec面向 JL 私有格式。 - 业务层(调用方):
AITranslationImpl通过内部类CustomAudioStreamCallback实现回调接口,把解码后的音频流接入翻译流水线;解码结果最终交给AudioPlayer播放。 - 底层依赖:蓝牙 SDK 提供
AudioData载体与Constants.AUDIO_TYPE_*类型常量,编解码库与 SDK 解耦,仅依赖其类型定义。
类层次结构
classDiagram
class AudioCodec {
#lastAudioData AudioData
+getAudioType() int
+getLastAudioData() AudioData
+isWorking() bool
+startDecodeStream(OnAudioStreamCallback callback)
+startDecodeStream(Object option, OnAudioStreamCallback callback)
+writeAudioData(AudioData audioData) bool
+stopDecodeStream() bool
+release()
}
class OnAudioStreamCallback {
<<interface>>
+onStart(int type)
+onStop(int type, String result)
+onError(int type, int code, String message)
+onStream(int srcType, int audioType, byte[] data)
}
class PcmCodec {
-mCallback OnAudioStreamCallback
-isWorking bool
+getAudioType() int
+startDecodeStream(Object option, OnAudioStreamCallback callback)
+writeAudioData(AudioData audioData) bool
+stopDecodeStream() bool
}
class OpusCodec {
-mDecoder OpusManager
+encodeFile(String pcmFilePath, String opusFilePath, OnStateCallback callback)$
+getAudioType() int
+isWorking() bool
+startDecodeStream(Object option, OnAudioStreamCallback callback)
}
class JLAV2Codec {
+getAudioType() int
}
AudioCodec <|-- PcmCodec
AudioCodec <|-- OpusCodec
AudioCodec <|-- JLAV2Codec
AudioCodec ..> OnAudioStreamCallback : "回调事件"
注:
JLAV2Codec的类声明已确认(public class JLAV2Codec extends AudioCodec),但其方法级实现细节未在本页读取范围内展开,详见源码文件。
实现详解
AudioCodec 抽象基类:编解码契约
AudioCodec 是整个编解码库的核心抽象,其设计意图是把"解码流"这一资源建模为有明确生命周期的对象。源码中的契约定义如下:
public abstract class AudioCodec {
/** 上一包音频数据 */
protected AudioData lastAudioData;
/** 音频类型 */
@AudioType
public abstract int getAudioType();
public AudioData getLastAudioData() {
return lastAudioData;
}
public abstract boolean isWorking();
public abstract void startDecodeStream(OnAudioStreamCallback callback);
public abstract void startDecodeStream(Object option, OnAudioStreamCallback callback);
public abstract boolean writeAudioData(AudioData audioData);
public abstract boolean stopDecodeStream();
public abstract void release();
public interface OnAudioStreamCallback {
void onStart(@AudioType int type);
void onStop(@AudioType int type, String result);
void onError(@AudioType int type, int code, String message);
void onStream(@AudioType int srcType, @AudioType int audioType, byte[] data);
}
}
Source: AudioCodec.java
设计要点:
lastAudioData受保护字段:由子类在writeAudioData时记录最近一包数据,供调试、状态恢复或重放使用。getLastAudioData()对任何调用方开放,但只有子类能写入,保证封装性。- 双
startDecodeStream重载:无参版本(startDecodeStream(OnAudioStreamCallback))是便捷入口,内部委托给带option的版本并传入默认参数;带option版本允许调用方传入格式相关配置(如OpusOption)。这是典型的"模板方法 + 参数对象"组合,让默认路径简单、高级路径可配置。 isWorking()幂等查询:业务层可随时轮询解码器是否处于工作状态,避免对已启动的流重复启动。release()资源回收:语义是释放解码器占用的所有资源(原生句柄、线程、回调引用),实现上通常委托给stopDecodeStream()并清理内部状态。OnAudioStreamCallback事件模型:四个事件覆盖解码流完整生命周期——onStart(启动成功)、onStream(输出一帧解码后数据)、onError(异常,携带错误码与消息)、onStop(正常停止,携带结果字符串)。onStream同时携带srcType(源格式)与audioType(输出格式),便于转码场景中区分输入输出。
PcmCodec:PCM 透传实现
PCM(Pulse Code Modulation)是未压缩的原始采样数据,因此 PcmCodec 的解码实质是透传——不经过任何变换,直接把收到的 AudioData 负载交给回调。其完整实现如下:
public class PcmCodec extends AudioCodec {
private OnAudioStreamCallback mCallback;
private boolean isWorking;
@Override
public int getAudioType() {
return Constants.AUDIO_TYPE_PCM;
}
@Override
public boolean isWorking() {
return isWorking;
}
@Override
public void startDecodeStream(OnAudioStreamCallback callback) {
startDecodeStream(null, callback);
}
@Override
public void startDecodeStream(Object option, OnAudioStreamCallback callback) {
if (isWorking()) {
if (null != callback) {
int code = ErrorCode.SUB_ERR_OPERATION_IN_PROGRESS;
callback.onError(getAudioType(), code, ErrorCode.code2Msg(code));
}
return;
}
mCallback = callback;
isWorking = true;
if (null != callback) {
callback.onStart(getAudioType());
}
}
@Override
public boolean writeAudioData(AudioData audioData) {
if (null == audioData || !isWorking() || audioData.getType() != getAudioType())
return false;
lastAudioData = audioData;
if (null != mCallback) {
mCallback.onStream(getAudioType(), Constants.AUDIO_TYPE_PCM, audioData.getAudioData());
}
return true;
}
@Override
public boolean stopDecodeStream() {
if (!isWorking()) return false;
isWorking = false;
final OnAudioStreamCallback callback = mCallback;
mCallback = null;
if (null != callback) {
callback.onStop(getAudioType(), "Success");
}
return true;
}
@Override
public void release() {
stopDecodeStream();
}
}
Source: PcmCodec.java
实现行为逐点分析:
- 状态守卫:
startDecodeStream在isWorking为 true 时拒绝重复启动,并通过ErrorCode.SUB_ERR_OPERATION_IN_PROGRESS(子错误码,表示"操作进行中")通知调用方。这保证了解码流单实例语义——同一时间只允许一个活跃解码流,避免回调被多个写入者并发触发。 - 启动语义:
startDecodeStream是同步操作,立即将isWorking置位并回调onStart。PCM 无需初始化任何原生资源,因此启动即就绪。 - 写入语义:
writeAudioData返回boolean,三重校验(非空、工作状态、类型匹配)任一不满足即返回false。类型校验audioData.getType() != getAudioType()防止把 OPUS 数据误写入 PCM 解码器。通过后记录lastAudioData并回调onStream,源类型与输出类型均为AUDIO_TYPE_PCM。 - 停止语义:
stopDecodeStream在未工作时返回false(幂等);正常停止时先取走mCallback引用并置空,再回调onStop("Success")——先置空再回调可防止回调内部再次触发写入导致的状态混乱。 release即停止:PCM 无底层资源,释放等价于停止流。
OpusCodec:OPUS 编解码实现
OpusCodec 是三种实现中最复杂的一个,它把独立库 com.jieli.jl_audio_decode 中的原生 OpusManager 封装进 AudioCodec 契约。其启动路径源码如下:
@Override
public void startDecodeStream(OnAudioStreamCallback callback) {
startDecodeStream(new OpusOption(), callback);
}
@Override
public void startDecodeStream(Object option, OnAudioStreamCallback callback) {
if (!(option instanceof OpusOption)) {
option = new OpusOption();
}
OpusOption opusOption = (OpusOption) option;
if (null == mDecoder) {
try {
mDecoder = new OpusManager();
} catch (OpusException e) {
String message = AppUtil.formatString("Failed to init opus manager.\n" +
"message : %s", e.getMessage());
if (null != callback) {
callback.onError(getAudioType(), ErrorCode.ERR_NONE_INIT, message);
}
return;
}
}
// ... 后续设置 option 并启动解码流(onStart 回调)
}
Source: OpusCodec.java
设计要点:
- 默认参数兜底:如果调用方传入的
option不是OpusOption实例,自动替换为new OpusOption()。这种"容错归一化"保证了解码器始终以合法配置启动,避免类型转换崩溃。 - 惰性创建 + 复用:
mDecoder(OpusManager)在首次启动时创建,后续启动复用同一实例,isWorking()委托给mDecoder.isDecodeStream()——工作状态由底层原生管理器维护,与PcmCodec的本地 boolean 标志形成对比。 - 初始化失败上报:
new OpusManager()可能抛出OpusException(原生库初始化失败,如 SO 加载失败),此时通过ErrorCode.ERR_NONE_INIT(未初始化错误码)回调onError,并携带格式化后的异常消息,便于上层定位问题。
OpusCodec 还提供离线文件编码能力,用于 PCM 文件到 OPUS 文件的转码:
public static void encodeFile(String pcmFilePath, String opusFilePath, OnStateCallback callback) {
try {
final OpusManager encoder = new OpusManager();
encoder.encodeFile(pcmFilePath, opusFilePath, new OnStateCallback() {
@Override
public void onStart() {
if (null != callback) {
callback.onStart();
}
}
@Override
public void onComplete(String s) {
encoder.release();
if (null != callback) {
callback.onComplete(s);
}
}
@Override
public void onError(int i, String s) {
encoder.release();
if (null != callback) {
callback.onError(i, s);
}
}
});
} catch (OpusException e) {
if (null != callback) {
callback.onError(ErrorCode.ERR_NONE_INIT, ErrorCode.getErrorMsg(ErrorCode.ERR_NONE_INIT));
}
}
}
Source: OpusCodec.java
该静态方法的关键行为:
- 确定性资源释放:无论编码成功(
onComplete)还是失败(onError),都先调用encoder.release()释放原生编码器,再向调用方转发回调。这一"先释放、后上报"的顺序保证异常路径不会泄漏原生资源。 - 初始化异常兜底:
new OpusManager()抛出的OpusException被捕获后,统一以ERR_NONE_INIT上报,错误码与解码路径保持一致。 - 回调转发模式:内部匿名
OnStateCallback只做空指针检查与转发,不添加业务逻辑,保持职责单一。
JLAV2Codec:JL AV2 编解码实现
JLAV2Codec 与 PcmCodec、OpusCodec 并列,同样继承 AudioCodec:
public class JLAV2Codec extends AudioCodec {
// 具体实现细节见源码文件
}
Source: JLAV2Codec.java
JL AV2 是杰理(Jieli)私有音频编码格式。该类的存在表明编解码库采用"一格式一策略"的扩展模式:新增格式只需新增子类并实现 getAudioType() 等契约方法,上层业务无需感知差异。其方法级实现(涉及 JL 私有解码器)未在本页读取范围内展开,具体细节以源码为准。
Core Flow
解码流生命周期
下图展示上层业务通过 AudioCodec 驱动一次完整解码流的时序(以 OPUS 为例):
sequenceDiagram
participant Biz as 业务层 (AITranslationImpl)
participant Codec as AudioCodec 子类 (OpusCodec)
participant Native as OpusManager (jl_audio_decode)
participant CB as OnAudioStreamCallback (CustomAudioStreamCallback)
Biz->>Codec: startDecodeStream(opusOption, callback)
Codec->>Native: new OpusManager()
Codec->>Native: 配置 OpusOption 并启动
Codec-->>CB: onStart(AUDIO_TYPE_OPUS)
CB-->>Biz: 更新状态 (解码中)
loop 每包音频数据
Biz->>Codec: writeAudioData(audioData)
Codec->>Codec: 校验类型 / 工作状态
Codec->>Native: 送入解码器
Native-->>Codec: 解码后 PCM 数据
Codec-->>CB: onStream(OPUS, PCM, byte[])
CB-->>Biz: 送入语音识别链路
end
Biz->>Codec: stopDecodeStream()
Codec->>Native: 停止并释放解码流
Codec-->>CB: onStop(AUDIO_TYPE_OPUS, "Success")
CB-->>Biz: 更新状态 (已停止)
流程要点:
- 启动:业务层调用
startDecodeStream,子类完成资源初始化(OPUS 需创建OpusManager;PCM 仅置标志位),随后同步回调onStart。 - 写入-解码循环:业务层逐包调用
writeAudioData(AudioData);子类校验数据合法性后送入底层解码,解码结果通过onStream回调输出。PCM 路径下该循环退化为纯透传。 - 停止:业务层调用
stopDecodeStream,子类停止底层流、清理回调引用并回调onStop。 - 异常路径:任何环节失败(如原生初始化异常)通过
onError(type, code, message)上报,业务层可据此终止流程或重试。
状态机视图
stateDiagram-v2
[*] --> IDLE
IDLE --> WORKING : startDecodeStream(callback)
IDLE --> IDLE : startDecodeStream (重复启动) → onError(SUB_ERR_OPERATION_IN_PROGRESS)
WORKING --> WORKING : writeAudioData(data) → onStream
WORKING --> IDLE : stopDecodeStream() → onStop
WORKING --> IDLE : release()
WORKING --> IDLE : onError (异常终止)
IDLE --> [*]
- IDLE(空闲):解码器未启动。
writeAudioData返回false,stopDecodeStream返回false(幂等)。 - WORKING(工作中):解码流活跃。重复
startDecodeStream被拒绝并以SUB_ERR_OPERATION_IN_PROGRESS上报错误;writeAudioData正常透传/解码并触发onStream。 - 状态迁移全部由子类内部维护(
PcmCodec用本地isWorking,OpusCodec委托OpusManager.isDecodeStream()),对上层透明。
数据模型
编解码库的数据载体是蓝牙 SDK 的 AudioData(com.jieli.bluetooth.bean.translation.AudioData)。AudioData 携带音频类型与字节负载,贯穿解码流全程:
erDiagram
AudioCodec {
AudioData lastAudioData "最近一包数据"
int audioType "格式标识"
}
AudioData {
int type "AUDIO_TYPE_*"
byte audioData "负载字节"
}
OnAudioStreamCallback {
int srcType "源格式"
int audioType "输出格式"
byte data "解码后数据"
}
AudioCodec ||--o{ AudioData : "持有/写入"
AudioCodec ||--o{ OnAudioStreamCallback : "回调输出"
| 成员 | 说明 |
|---|---|
AudioData.type | 音频类型,取值来自 Constants.AUDIO_TYPE_*(如 AUDIO_TYPE_PCM、AUDIO_TYPE_OPUS),由 @AudioType 注解约束 |
AudioData.getAudioData() | 音频负载字节数组,writeAudioData 与 onStream 均操作该负载 |
类型常量对照:PcmCodec.getAudioType() 返回 Constants.AUDIO_TYPE_PCM;OpusCodec.getAudioType() 返回 Constants.AUDIO_TYPE_OPUS。onStream 回调中 srcType 与 audioType 的组合(如 OPUS → PCM)体现了"源格式 → 输出格式"的转码关系——OPUS 解码后输出 PCM 数据,而 PCM 路径二者相同。
Usage Examples
示例一:业务层实现回调并驱动解码流
AITranslationImpl 通过内部静态类实现 OnAudioStreamCallback,展示业务层接入编解码库的标准姿势:
private static class CustomAudioStreamCallback implements AudioCodec.OnAudioStreamCallback {
// onStart / onStop / onError / onStream 四类事件的业务处理
// 其中 onStream 接收解码后的音频数据并送入后续处理链路
}
Source: AITranslationImpl.java
接入步骤(与源码结构对应):
- 根据音频类型选择
AudioCodec子类(PCM →PcmCodec,OPUS →OpusCodec,AV2 →JLAV2Codec); startDecodeStream(option, callback)启动解码流,在onStart中确认就绪;- 持续
writeAudioData(audioData)喂入数据,在onStream中消费解码结果; - 结束或异常时
stopDecodeStream(),在onStop/onError中收尾。
示例二:PCM 解码器的最小驱动序列
PcmCodec pcmCodec = new PcmCodec();
pcmCodec.startDecodeStream(callback); // 触发 onStart(AUDIO_TYPE_PCM)
boolean ok = pcmCodec.writeAudioData(audioData); // 校验通过 → onStream(PCM, PCM, data)
pcmCodec.stopDecodeStream(); // 触发 onStop(AUDIO_TYPE_PCM, "Success")
pcmCodec.release(); // 等价于 stopDecodeStream()
Source: PcmCodec.java
示例三:OPUS 离线文件编码
OpusCodec.encodeFile(pcmFilePath, opusFilePath, new OnStateCallback() {
@Override
public void onStart() { /* 编码开始 */ }
@Override
public void onComplete(String s) { /* 编码完成,编码器已自动释放 */ }
@Override
public void onError(int i, String s) { /* 编码失败,编码器已自动释放 */ }
});
Source: OpusCodec.java
该示例演示了 encodeFile 的"回调内无需手动释放编码器"的便利性:onComplete/onError 回调触发前,内部已调用 encoder.release(),调用方只需处理业务结果。
示例四:OPUS 解码流启动(带参数)
OpusCodec opusCodec = new OpusCodec();
OpusOption option = new OpusOption(); // 可自定义采样率等参数
opusCodec.startDecodeStream(option, callback); // 内部创建 OpusManager 并启动
while (opusCodec.isWorking()) {
opusCodec.writeAudioData(opusAudioData); // 逐包喂入
}
opusCodec.stopDecodeStream();
Source: OpusCodec.java
Configuration Options
编解码库的配置通过 startDecodeStream(Object option, ...) 的 option 参数注入,目前仅有 OPUS 路径使用配置对象:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
startDecodeStream 的 option | Object | 各子类默认(PCM 忽略,OPUS 为 new OpusOption()) | 解码流启动参数;非 OpusOption 实例会被自动归一化为默认值 |
OpusOption | OpusOption(来自 jl_audio_decode) | new OpusOption() | OPUS 解码器参数(采样率、声道等),字段定义见底层库源码 |
startDecodeStream(callback) 便捷重载 | - | 委托带参版本 | 使用默认参数启动,无需关心格式细节 |
设计意图:默认参数路径保证"零配置可用",带参路径为高级场景(如指定采样率)留出扩展空间;错误的参数类型不会导致崩溃,而是静默回退到默认配置。
API Reference
AudioCodec(抽象基类)
int getAudioType()
- 描述:返回该编解码器处理的音频格式,标注
@AudioType,取值来自蓝牙 SDK 的Constants.AUDIO_TYPE_*。 - 返回:音频类型常量(
AUDIO_TYPE_PCM/AUDIO_TYPE_OPUS等)。
AudioData getLastAudioData()
- 描述:返回最近一次成功写入的音频数据包,可用于调试或重放。
- 返回:
AudioData或null(从未写入)。
boolean isWorking()
- 描述:查询解码流是否处于工作状态。
PcmCodec由本地标志维护,OpusCodec委托OpusManager.isDecodeStream()。 - 返回:
true表示解码流活跃。
void startDecodeStream(OnAudioStreamCallback callback)
- 描述:使用默认参数启动解码流。
- 参数:
callback— 事件回调,可为空(空则静默启动)。 - 副作用:成功后回调
onStart(getAudioType());若已工作中,回调onError(type, SUB_ERR_OPERATION_IN_PROGRESS, ...)。
void startDecodeStream(Object option, OnAudioStreamCallback callback)
- 描述:使用指定参数启动解码流。
OpusCodec会校验option类型,非法值回退为new OpusOption();OpusCodec初始化失败时回调onError(type, ERR_NONE_INIT, message)。 - 参数:
option— 格式相关配置对象;callback— 事件回调。 - 返回:无(结果通过回调上报)。
boolean writeAudioData(AudioData audioData)
- 描述:向解码流写入一包音频数据。写入成功记录
lastAudioData并触发onStream。 - 参数:
audioData— 音频数据包。 - 返回:
true写入成功;false当数据为空、未在工作状态或类型不匹配。
boolean stopDecodeStream()
- 描述:停止解码流并触发
onStop(type, "Success");未工作时返回false(幂等)。 - 返回:
true本次确实停止了流。
void release()
- 描述:释放编解码器资源。
PcmCodec等价于stopDecodeStream();OpusCodec额外释放底层OpusManager(见底层库实现)。
OnAudioStreamCallback(接口)
| 方法 | 触发时机 | 关键参数 |
|---|---|---|
void onStart(@AudioType int type) | 解码流启动成功 | type:本解码器格式 |
void onStop(@AudioType int type, String result) | 正常停止 | result:结果描述(如 "Success") |
void onError(@AudioType int type, int code, String message) | 启动/运行异常 | code:错误码(SUB_ERR_OPERATION_IN_PROGRESS、ERR_NONE_INIT 等);message:可读信息 |
void onStream(@AudioType int srcType, @AudioType int audioType, byte[] data) | 每输出一帧解码数据 | srcType:源格式;audioType:输出格式;data:解码后负载 |
OpusCodec(静态工具方法)
static void encodeFile(String pcmFilePath, String opusFilePath, OnStateCallback callback)
- 描述:将 PCM 文件编码为 OPUS 文件(离线转码)。
- 参数:
pcmFilePath— PCM 源文件路径;opusFilePath— OPUS 目标文件路径;callback— 底层库的OnStateCallback状态回调(onStart/onComplete/onError)。 - 资源保证:无论成功失败,内部编码器都会在回调前释放。
- Throws:
OpusException(原生初始化失败)被内部捕获并转为onError(ERR_NONE_INIT, ...)回调。
Failure Modes, Edge Cases & Concurrency
已确认的错误处理路径
| 场景 | 处理方式 | 证据 |
|---|---|---|
| 重复启动解码流 | 拒绝并回调 onError(SUB_ERR_OPERATION_IN_PROGRESS) | PcmCodec.java L42-L48 |
| 写入空数据 / 未启动 / 类型不匹配 | writeAudioData 返回 false,不触发回调 | PcmCodec.java L57-L59 |
OPUS 原生管理器初始化失败(OpusException) | 回调 onError(ERR_NONE_INIT, 格式化消息) | OpusCodec.java L91-L99 |
encodeFile 初始化失败 | 捕获 OpusException 并回调 onError(ERR_NONE_INIT) | OpusCodec.java L57-L61 |
非法 option 参数类型 | 静默回退为默认 OpusOption() | OpusCodec.java L87-L89 |
| 未启动时停止 / 释放 | 幂等,返回 false,无回调 | PcmCodec.java L68-L69 |
边界情况
- 空回调:
startDecodeStream(null)允许——内部所有回调前都有空指针检查,解码流仍可启动,但业务层收不到任何事件(仅适合纯写入的丢弃场景)。 - 类型失配写入:
writeAudioData的类型校验保证 OPUS 数据不会进入PcmCodec,但不保证调用方传入了正确的解码器实例——选择解码器是上层职责(按getAudioType()匹配)。 - 重复
release():release()委托stopDecodeStream(),后者幂等,重复释放安全。
并发与一致性
- 单活跃流约束:
startDecodeStream的重复启动守卫(SUB_ERR_OPERATION_IN_PROGRESS)隐含"单解码流"设计——同一编解码器实例同一时刻只允许一个活跃流,回调不会因多写入者交错而错乱。 - 回调引用快照:
stopDecodeStream先取走mCallback引用再置空,避免停止过程中回调触发新的写入导致NPE或状态倒挂。 - 线程模型:源码未显示内部线程池/锁;
writeAudioData的回调在调用线程同步执行。上层若从多线程喂数据,需自行保证串行化(例如通过单一音频接收线程),本库不提供线程安全保障。
Performance & Operational Considerations
- PCM 路径零拷贝透传:
PcmCodec.writeAudioData不进行任何数据变换,onStream直接透传audioData.getAudioData(),开销极低,适合高频原始采样流。 - OPUS 解码为原生调用:
OpusCodec委托OpusManager(jl_audio_decode原生库),解码耗时为 JNI 调用;isWorking()每次查询都进入底层,高频轮询有额外开销,建议以事件驱动(onStop/onError)代替轮询。 - 解码器实例复用:
OpusCodec.mDecoder惰性创建并复用,避免每次启动都重新加载原生库;但实例与回调生命周期绑定,release()后需重新初始化。 encodeFile异步语义:OpusCodec.encodeFile是异步文件操作,回调线程为底层库内部线程;调用方不应在回调中执行耗时 UI 操作。- 资源释放时机:OPUS 路径务必在业务结束或
onError后调用release(),确保原生解码器句柄及时回收;PcmCodec的release()仅为状态清理,成本可忽略。
Extension Points
编解码库的扩展模型是"一格式一策略",新增音频格式的步骤非常清晰:
- 新增子类:创建
XxxCodec extends AudioCodec,置于com.jieli.btsmart.tool.translate.codec包内(或同包可见位置); - 实现契约:实现
getAudioType()(返回新的@AudioType常量)、isWorking()、两个startDecodeStream重载、writeAudioData()、stopDecodeStream()、release(); - 接入格式调度:在上层(如
AITranslationImpl)按音频类型分发到新子类——若类型选择逻辑集中,可考虑引入工厂/注册表,但当前源码中未发现独立工厂类(选择逻辑内联在业务层); - 可选:若新格式需要参数配置,仿照
OpusOption定义参数对象,并在startDecodeStream(Object option, ...)中做类型校验与默认值回退。
扩展约定(来自现有实现的隐含约束):
- 子类必须保证
startDecodeStream的幂等拒绝语义(重复启动上报SUB_ERR_OPERATION_IN_PROGRESS); - 子类必须在
stopDecodeStream()中先清理回调引用再回调onStop; release()必须可重复调用且幂等;- 原生资源(如 OPUS 管理器)必须在异常路径同样释放,遵循"先释放、后上报"模式。
Tests
本页读取范围内未在 tool/translate/codec 包中发现独立单元测试文件。测试保障主要来自:
- 上层集成验证:
AITranslationImpl.CustomAudioStreamCallback对onStream数据的消费逻辑,实际覆盖了解码流端到端行为; - 契约自洽性:
PcmCodec的三重校验(空值/工作状态/类型匹配)与幂等停止逻辑,天然可被单元测试覆盖(构造非法AudioData断言返回false、重复启动断言onError等),但当前仓库中未见对应测试用例,属于文档化差距,补充测试时建议优先覆盖:重复启动拒绝、类型失配写入拒绝、停止后写入拒绝、release幂等。
Related Links
- AudioCodec.java(抽象基类)
- PcmCodec.java(PCM 实现)
- OpusCodec.java(OPUS 实现)
- JLAV2Codec.java(JL AV2 实现)
- AITranslationImpl.java(业务调用方)
- AudioPlayer.java(播放器)
- TranslationSessionPlayer.java(翻译会话播放器)
- 相关主题:音频播放与翻译会话流程见播放器页面;音频采集与上传见音频处理相关页面;底层
AudioData与@AudioType定义见蓝牙 SDK 文档。