均衡器音效调节
均衡器(EQ)音效调节是杰理蓝牙 SDK 应用层的重要音频处理能力,涵盖预设音效模式、自定义增益调节、EQ 曲线可视化以及混响/动态限幅等扩展音效。本文档基于 btsmart 模块中的 EqCacheUtil、EqDemo 与 BluetoothHelper 等源码,完整说明该能力从蓝牙指令下发、参数缓存到界面绘制的端到端实现机制。
Purpose and Scope
本页面深入介绍「均衡器音效调节」这一能力的完整实现,包括:
- 7 种预设 EQ 模式及其默认增益值(Normal、Rock、Pop、Classical、Jazz、Country、Custom)
EqCacheUtil的本地缓存机制(SharedPreferences + Gson 序列化)- EQ 曲线位图渲染管线(
EQPlotCore+EqCovertUtil+ Canvas) - 基于
RCSPController的蓝牙指令通道(查询/配置 EQ 信息) - 扩展音效:混响(Reverberation)与动态限幅(Dynamic Limiter)
以下相关主题属于其他目录页面的范围,本页不展开:蓝牙连接管理(BluetoothHelper 的连接生命周期)、其他音频处理能力(如音效模式 AdjustVoiceMode)、设备设置页整体导航。
Overview
在杰理蓝牙方案中,均衡器调节本质上是一个「应用层 ↔ 蓝牙设备」的双向参数同步过程:
- 指令通道:App 通过
RCSPController向已连接的蓝牙设备(耳机/音箱)发送 EQ 查询或配置命令,设备端 DSP 据此调整音频频响曲线; - 本地缓存:为避免每次启动都重新拉取、也为了在设备断连后保留用户偏好,App 使用
EqCacheUtil将各模式的增益值序列化为 JSON 存入 SharedPreferences; - 可视化:App 将 EQ 增益数组与频率点换算成屏幕坐标,用
EQPlotCore计算频响插值点、EqCovertUtil做坐标变换,最终以位图形式缓存 EQ 曲线,供列表/详情界面复用。
关键设计意图:缓存与渲染分离。EqCacheUtil 只负责"数据"(预设值、当前值、JSON 字符串),位图渲染独立成 createBitmap() 方法,并以"值变化即失效删除"的策略保证缓存一致性——只有 EQ 增益值真正变化时才重绘曲线,避免无谓的 I/O 与绘制开销。
Architecture
flowchart TD
subgraph sg_UI["UI 层"]
FxFragment["EQ 调节界面 Fragment"]
Adapter["列表/曲线 Adapter"]
end
subgraph sg_Cache["缓存层"]
EqCacheUtil["EqCacheUtil"]
Pref["PreferencesHelper / SharedPreferences"]
BmpFile["EQ 曲线位图<br/>(外部缓存目录)"]
end
subgraph sg_Render["渲染层"]
EqPlot["EQPlotCore (频响插值)"]
EqConvert["EqCovertUtil (坐标变换)"]
end
subgraph sg_Channel["指令通道层"]
BluetoothHelper["BluetoothHelper"]
RCSP["RCSPController (SDK)"]
EventCB["BTRcspEventCallback"]
end
subgraph sg_Device["设备端"]
Dev["蓝牙耳机/音箱 DSP"]
end
FxFragment -->|"读写缓存"| EqCacheUtil
EqCacheUtil -->|"JSON/字符串"| Pref
EqCacheUtil -->|"生成/删除"| BmpFile
EqCacheUtil -->|"绘制曲线"| EqPlot
EqPlot --> EqConvert
FxFragment -->|"查询/配置命令"| BluetoothHelper
BluetoothHelper -->|"RCSP 命令"| RCSP
RCSP -->|"BLE 数据通道"| Dev
Dev -->|"EQ 数据回传"| RCSP
RCSP -->|"事件回调"| EventCB
EventCB -->|"刷新界面"| FxFragment
架构分为四个层次,各层职责单一、单向依赖:
- UI 层:EQ 调节界面与列表 Adapter,只关心"展示什么"与"用户改了什么",不直接接触蓝牙协议;
- 缓存层:
EqCacheUtil统一封装预设值、当前值的持久化与失效清理,所有读写都经过PreferencesHelper(SharedPreferences 封装)与 Gson; - 渲染层:
EQPlotCore(来自com.jieli.eq库)根据频率点与增益值计算频响插值数据,EqCovertUtil将物理坐标点映射到位图画布,最终由Canvas.drawLines画出折线; - 指令通道层:
BluetoothHelper封装 RCSP 命令构造(如CommandBuilder.buildGetEqValueCmd()),RCSPController负责 BLE 传输与解析,设备端数据变化通过BTRcspEventCallback上抛到 UI。
预设 EQ 模式与默认增益值
EqCacheUtil 在类加载时定义了一组静态预设,这是整个 EQ 能力的"出厂默认值"来源:
public final static byte[] EQ_NORMAL = new byte[10];
public final static byte[] EQ_ROCK = new byte[]{-2, 0, 2, 4, -2, -2, 0, 0, 4, 4};
public final static byte[] EQ_POP = new byte[]{3, 1, 0, -2, -4, -4, -2, 0, 1, 2};
public final static byte[] EQ_CLASSICAL = new byte[]{0, 8, 8, 4, 0, 0, 0, 0, 2, 2};
public final static byte[] EQ_JAZZ = new byte[]{0, 0, 0, 4, 4, 4, 0, 2, 3, 4};
public final static byte[] EQ_COUNTRY = new byte[]{-2, 0, 0, 2, 2, 0, 0, 0, 4, 4};
public final static byte[] EQ_CUSTOM = new byte[10];
public final static byte[][] EQ_VALUES = new byte[][]{EQ_NORMAL, EQ_ROCK, EQ_POP, EQ_CLASSICAL, EQ_JAZZ, EQ_COUNTRY, EQ_CUSTOM};
设计要点:
- 模式索引即数组下标:
EQ_VALUES[i]中的i同时是EqInfo.getMode()的取值,二者通过下标强绑定,保证getCacheEqInfo(modeIndex)等接口的映射关系简单且不易出错; - 10 个频段:每个模式的增益数组固定为 10 个
byte,对应BluetoothConstant.DEFAULT_EQ_FREQS提供的 10 个频率点(见getPresetEqInfo()中的eqInfo.setFreqs(BluetoothConstant.DEFAULT_EQ_FREQS)); - 增益单位为 dB 偏移:数值为有符号 byte(如
-2表示衰减 2dB、8表示提升 8dB),EQ_NORMAL全 0 表示平直响应,EQ_CUSTOM初始也为全 0,由用户在界面自由拖动; - Preset(出厂预置)与 Cache(本地缓存)分离:
getPresetEqInfo()在没有本地缓存时,会用EQ_VALUES构造一个EqPresetInfo(7 个EqInfo,每个都挂上默认频率点)作为设备端的预设参考;用户修改后通过savePresetEqInfo()以 JSON 覆盖持久化。
各模式增益对照表
| 模式 | mode 值 | 10 段增益(dB) |
|---|---|---|
| Normal(正常) | 0 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 |
| Rock(摇滚) | 1 | -2, 0, 2, 4, -2, -2, 0, 0, 4, 4 |
| Pop(流行) | 2 | 3, 1, 0, -2, -4, -4, -2, 0, 1, 2 |
| Classical(古典) | 3 | 0, 8, 8, 4, 0, 0, 0, 0, 2, 2 |
| Jazz(爵士) | 4 | 0, 0, 0, 4, 4, 4, 0, 2, 3, 4 |
| Country(乡村) | 5 | -2, 0, 0, 2, 2, 0, 0, 0, 4, 4 |
| Custom(自定义) | 6 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 |
EqCacheUtil 缓存机制
EqCacheUtil 是一个纯静态工具类(无实例状态),围绕三条 SharedPreferences 键组织缓存:
| 键前缀 | 用途 | 内容格式 |
|---|---|---|
KEY_EQ_VALUE("KEY_EQ_VALUE") | 按模式保存的 EQ 值,键为 KEY_EQ_VALUE + mode | EqInfo 的 Gson JSON |
KEY_EQ_CURRENT_VALUE | 当前生效的 EQ 值(全局唯一键) | EqInfo 的 Gson JSON |
KEY_EQ_PRESET | 设备预设 EQ 信息 | EqPresetInfo 的 Gson JSON |
public static void saveEqValue(EqInfo eqInfo) {
String lastValue = getEqInfoString(eqInfo);
String currentValue = eqInfo2String(eqInfo);
//上次的值和本次值不同时删除生成的图片且自定义不生成图片
if (!lastValue.equals(currentValue) && eqInfo.getMode() != 6) {
String path = generateCacheBitmapPath(eqInfo);
FileUtil.deleteFile(new File(path));
}
PreferencesHelper.putStringValue(AppUtil.getContext(), getStatusKey(KEY_EQ_VALUE + eqInfo.getMode()), currentValue);
PreferencesHelper.putStringValue(AppUtil.getContext(), getStatusKey(KEY_EQ_CURRENT_VALUE), currentValue);
}
saveEqValue() 的失效策略值得细读:
- 先比较"上次缓存值"与"本次值"的 JSON 字符串,仅当真正发生变化时才删除旧的曲线位图,避免每次保存都触发文件删除;
- 特判
mode == 6(Custom):自定义模式不生成也不维护缓存位图,因为自定义曲线由用户实时拖动,走的是实时绘制路径而非位图缓存; - 写入时同时更新「按模式键」和「当前值键」,前者供切换模式时快速恢复,后者供下次启动时恢复上次生效的 EQ。
读取侧对应提供按模式与按当前的两种入口:
public static EqInfo getCacheEqInfo(int modeIndex) {
String eqInfoStr = PreferencesHelper.getSharedPreferences(AppUtil.getContext()).getString(getStatusKey(KEY_EQ_VALUE + modeIndex), "");
if (TextUtils.isEmpty(eqInfoStr)) {
return new EqInfo(modeIndex, EQ_VALUES[modeIndex]);
}
return string2eqInfo(eqInfoStr);
}
public static EqInfo getCurrentCacheEqInfo() {
String string = PreferencesHelper.getSharedPreferences(AppUtil.getContext()).getString(getStatusKey(KEY_EQ_CURRENT_VALUE), "");
if (TextUtils.isEmpty(string)) {
return new EqInfo(0, new byte[10]);
}
return string2eqInfo(string);
}
兜底逻辑:缓存为空(首次启动或已被清除)时,getCacheEqInfo 直接用 EQ_VALUES[modeIndex] 构造 EqInfo 作为默认;getCurrentCacheEqInfo 则退回 Normal 模式(mode=0)全零增益。这一设计保证 UI 层永远拿得到合法对象,无需空指针判断。
预设信息(EqPresetInfo)的读写
public static EqPresetInfo getPresetEqInfo() {
String string = PreferencesHelper.getSharedPreferences(AppUtil.getContext()).getString(getStatusKey(KEY_EQ_PRESET), "");
if (TextUtils.isEmpty(string)) {
EqPresetInfo eqPresetInfo = new EqPresetInfo();
eqPresetInfo.setNumber(7);
List<EqInfo> eqInfos = new ArrayList<>();
for (int i = 0; i < 7; i++) {
EqInfo eqInfo = new EqInfo();
eqInfo.setMode(i);
eqInfo.setValue(EQ_VALUES[i].clone());
eqInfo.setFreqs(BluetoothConstant.DEFAULT_EQ_FREQS);
eqInfos.add(eqInfo);
}
eqPresetInfo.setEqInfos(eqInfos);
return eqPresetInfo;
}
return sGson.fromJson(string, EqPresetInfo.class);
}
EqPresetInfo 是 SDK 侧(com.jieli.bluetooth.bean.device.eq)定义的设备预设结构:包含 number(预设个数)与 eqInfos(EqInfo 列表)。无缓存时用 EQ_VALUES[i].clone() 填充——注意这里刻意使用 clone() 而非直接引用,防止外部修改 EQ_VALUES 静态数组污染全局默认值,这是多界面并发访问下保护常量数据的细节。
缓存清理
public static void clear() {
PreferencesHelper.remove(AppUtil.getContext(), getStatusKey(KEY_EQ_CURRENT_VALUE));
PreferencesHelper.remove(AppUtil.getContext(), getStatusKey(KEY_EQ_PRESET));
}
clear() 只清理"当前值"与"预设值",保留各模式的 KEY_EQ_VALUE + mode 缓存。MainApplication.init() 在每次 App 启动时调用 EqCacheUtil.clear()(注释说明"暂时重新打开 app 时重置 eq 缓存,如有需要可以放开"),即当前策略是每次冷启动重置 EQ 状态,避免设备切换后残留旧设备的 EQ 配置。
另外注意 getStatusKey() 中有一段被注释掉的逻辑:原设计意图是让缓存键带上已连接设备的蓝牙地址(key + "-" + device.getAddress()),实现按设备隔离的 EQ 记忆,当前版本临时改为直接返回原键。这属于预留的扩展点(详见后文"Extension Points")。
EQ 曲线位图渲染
EQ 曲线可视化由 getEqValueBitMapPath(EqInfo) 驱动:先按"增益值+频率点"生成确定性文件路径,文件存在直接复用,否则调用 createBitmapAndSave() 实时绘制并落盘:
public static String getEqValueBitMapPath(EqInfo eqInfo) {
String path = generateCacheBitmapPath(eqInfo);
JL_Log.e("sen", "path--->" + path);
File file = new File(path);
if (file.exists()) {
return path;
}
return createBitmapAndSave(eqInfo);
}
路径生成规则把"内容哈希"直接编码进文件名,天然实现内容寻址式缓存:
private static String generateCacheBitmapPath(EqInfo eqInfo) {
StringBuilder sb = new StringBuilder();
sb.append(AppUtil.getContext().getExternalCacheDir().getPath())
.append(File.separator)
.append("eq=")
.append(CHexConver.byte2HexStr(eqInfo.getValue(), eqInfo.getValue().length));
for (int freq : eqInfo.getFreqs()) {
sb.append(freq);
}
return sb.toString();
}
曲线绘制采用两段式管线:
public static Bitmap createBitmap(EqInfo eqInfo) {
int w = ValueUtil.dp2px(AppUtil.getContext(), 250);
int h = ValueUtil.dp2px(AppUtil.getContext(), 50);
//点计算
int[] freqs = eqInfo.getFreqs();
byte[] values = eqInfo.getValue();
EQPlotCore eqPlotCore = new EQPlotCore(w, freqs.length, freqs);
float[] pData = new float[4 * (w - 2) + 4];
for (int i = 0; i < freqs.length && i < values.length; i++) {
eqPlotCore.updatePara(i, freqs[i], values[i]);
eqPlotCore.getEQPlotData(pData, i);
}
//点转换
EqCovertUtil eqCovertUtil = new EqCovertUtil(w, h);
float[] sData = eqCovertUtil.pPoint2SPoint(pData);
//图片绘制
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.WHITE);
Paint paint = new Paint();
paint.setStrokeWidth(ValueUtil.dp2px(AppUtil.getContext(), 2));
paint.setColor(AppUtil.getContext().getResources().getColor(R.color.colorAccent));
canvas.drawLines(sData, paint);
return bitmap;
}
渲染细节与设计取舍:
- 固定画布尺寸:250dp × 50dp、
RGB_565位图格式,兼顾列表缩略图清晰度与内存占用; - 频响插值:
EQPlotCore(杰理com.jieli.eq库)逐个频率点updatePara(i, freq, value)后产出4 * (w - 2) + 4个浮点数据(每像素 4 个坐标值 + 收尾点),即把离散的 10 个频点插值成连续曲线; - 坐标变换:
EqCovertUtil.pPoint2SPoint()将插值点从"频响物理坐标"映射到位图像素坐标; - 绘制:
canvas.drawLines(sData, paint)一次调用批量画线,线宽 2dp、主题色colorAccent、白底——视觉上形成简洁的"白色卡片 + 主题色 EQ 折线"列表样式。
蓝牙指令通道
UI 层不直接使用 RCSPController,而是经由 BluetoothHelper 这一门面。它把"构造命令 + 发送 + 结果分发"封装成简单方法:
public void getEqInfo(final CommandCallback commandCallback) {
sendCommand(CommandBuilder.buildGetEqValueCmd(), new GetSysCommandCallback(mCallbackManager, commandCallback));
}
该实现体现了两层设计:
CommandBuilder.buildGetEqValueCmd()负责把"查询 EQ 值"这一语义编译成 RCSP 协议字节流;GetSysCommandCallback(mCallbackManager, commandCallback)把 SDK 的系统命令回调统一桥接到mCallbackManager,业务方只需关心CommandCallback,无需理解协议细节。
BluetoothHelper 在更新设备信息(如重连后恢复状态)时也会同步保留 EQ 相关字段:
.setEqInfo(oldDevInfo.getEqInfo())
.setEqPresetInfo(oldDevInfo.getEqPresetInfo())
这意味着设备信息对象(DevInfo)携带 EqInfo 与 EqPresetInfo 快照,重连/状态恢复时不丢 EQ 状态。
RCSPController 与事件回调
SDK 侧由 RCSPController(单例)提供命令执行能力,EQ 相关 API 与事件在官方 Demo 中有完整示范(见下节代码示例),核心 API 如下:
| API | 作用 | 结果通知方式 |
|---|---|---|
getEqInfo(device, callback) | 查询设备当前 EQ 信息与预设 | onEqPresetChange / onEqChange 事件 + 命令回调 |
configEqInfo(device, eqInfo, callback) | 下发 EQ 配置(模式+增益)到设备 | 命令回调 + onEqChange 事件 |
getExpandDataInfo(device, callback) | 获取扩展音效信息(含混响/动态限幅参数) | onExpandFunction 事件 |
setExpandDataInfo(device, mask, data, callback) | 按功能掩码写入扩展音效参数 | 命令回调 |
setReverberationParameter(device, param, callback) | 设置混响参数 | 命令回调 |
setDynamicLimiterParameter(device, param, callback) | 设置动态限幅参数 | 命令回调 |
事件侧统一由 BTRcspEventCallback 上报设备主动推送或查询结果,UI 层在其 onEqChange / onEqPresetChange / onExpandFunction 回调中刷新界面并同步本地缓存。这种"命令-事件"双通道模式是 RCSP 协议的标准范式:命令回调只保证"指令已执行",真实数据通过事件送达,避免 UI 阻塞等待。
Core Flow
查询 EQ 信息(App 启动/进入 EQ 页)
sequenceDiagram
participant UI as EQ 界面
participant CH as BluetoothHelper
participant RC as RCSPController
participant DEV as 蓝牙设备
participant CB as BTRcspEventCallback
participant CACHE as EqCacheUtil
UI->>CACHE: getPresetEqInfo()/getCurrentCacheEqInfo()
CACHE-->>UI: 本地预设/当前值(无缓存则返回默认)
UI->>CH: getEqInfo(callback)
CH->>RC: buildGetEqValueCmd() 下发查询命令
RC->>DEV: BLE 写特征值
DEV-->>RC: 设备回传 EQ 数据
RC-->>CB: onEqPresetChange(device, EqPresetInfo)
RC-->>CB: onEqChange(device, EqInfo)
CB-->>UI: 刷新界面并 saveEqValue() 更新缓存
流程说明:
- 界面先读本地缓存立即渲染(首帧不依赖蓝牙往返,减少白屏);
- 同时通过
BluetoothHelper.getEqInfo()向设备发起查询; - 设备回传后,SDK 触发
BTRcspEventCallback.onEqPresetChange与onEqChange两个事件; - UI 用设备权威数据覆盖本地值,并调用
EqCacheUtil.saveEqValue()保持缓存与设备一致。
配置 EQ 参数(用户拖动增益/切换模式)
sequenceDiagram
participant UI as EQ 界面
participant RC as RCSPController
participant DEV as 蓝牙设备
participant CB as BTRcspEventCallback
UI->>UI: 用户切换模式/拖动滑杆
UI->>RC: configEqInfo(device, eqInfo, callback)
RC->>DEV: 下发 EQ 配置命令
DEV-->>RC: 命令确认
RC-->>UI: onSuccess(device, Boolean)
RC-->>CB: onEqChange(device, eqInfo) 设备确认后事件
CB-->>UI: 同步 UI 状态与缓存
用户操作与设备确认之间存在异步窗口:configEqInfo 的 onSuccess 只表示指令被设备接收,真正的生效状态以 onEqChange 事件为准,UI 层应以此为准更新滑杆位置,避免"本地已改、设备未生效"的显示漂移。
Usage Examples
查询均衡器信息
官方 Demo 展示了完整的查询流程——注册事件监听、执行命令、在事件回调中接收数据:
void getEqInfo() {
//获取RCSPController对象
RCSPController controller = RCSPController.getInstance();
//添加蓝牙RCSP事件监听器
controller.addBTRcspEventCallback(new BTRcspEventCallback() {
@Override
public void onEqPresetChange(BluetoothDevice device, EqPresetInfo eqPresetInfo) {
//此处将会回调均衡器预设值
}
@Override
public void onEqChange(BluetoothDevice device, EqInfo eqInfo) {
//此处将会回调均衡器效果信息
}
});
//执行获取均衡器信息功能并等待结果回调
controller.getEqInfo(controller.getUsingDevice(), new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
//成功回调
//结果将会在BTRcspEventCallback#onEqPresetChange、BTRcspEventCallback#onEqChange回调
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
//失败回调
//error - 错误信息
}
});
}
来源:EqDemo.java
配置均衡器效果
void configEqInfo(EqInfo eqInfo) {
//获取RCSPController对象
RCSPController controller = RCSPController.getInstance();
//添加蓝牙RCSP事件监听器
controller.addBTRcspEventCallback(new BTRcspEventCallback() {
@Override
public void onEqChange(BluetoothDevice device, EqInfo eqInfo) {
//此处将会回调均衡器效果信息
}
});
//执行配置均衡器效果功能并等待结果回调
controller.configEqInfo(controller.getUsingDevice(), eqInfo, new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
//成功回调
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
//失败回调
//error - 错误信息
}
});
}
来源:EqDemo.java
扩展音效:混响与动态限幅
扩展音效走 getExpandDataInfo / setExpandDataInfo 通道或直接设置参数对象:
void setReverberationParameter(ReverberationParam param){
//获取RCSPController对象
RCSPController controller = RCSPController.getInstance();
//执行设置混响功能并等待结果回调
controller.setReverberationParameter(controller.getUsingDevice(), param, new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
//成功回调
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
//失败回调
//error - 错误信息
}
});
}
void setDynamicLimiterParameter(DynamicLimiterParam param){
//获取RCSPController对象
RCSPController controller = RCSPController.getInstance();
//执行设置动态限幅功能并等待结果回调
controller.setDynamicLimiterParameter(controller.getUsingDevice(), param, new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
//成功回调
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
//失败回调
//error - 错误信息
}
});
}
来源:EqDemo.java
ReverberationParam 与 DynamicLimiterParam 均为 SDK 定义的数据类(com.jieli.bluetooth.bean.device.eq),App 侧只需构造参数对象并提交,协议序列化由 SDK 完成。
本地缓存读写与曲线位图
// 读取某模式缓存(无缓存返回预设默认)
EqInfo eqInfo = EqCacheUtil.getCacheEqInfo(EqCacheUtil.EQ_VALUES.length > 1 ? 1 : 0);
// 保存当前 EQ 值(自动失效旧曲线位图)
EqCacheUtil.saveEqValue(eqInfo);
// 获取 EQ 曲线位图路径(不存在则自动生成并保存)
String bmpPath = EqCacheUtil.getEqValueBitMapPath(eqInfo);
API Reference
EqCacheUtil(com.jieli.btsmart.util.EqCacheUtil)
| 方法 | 返回 | 说明 |
|---|---|---|
saveEqValue(EqInfo eqInfo) | void | 保存指定 EQ 值到缓存;值变化时删除旧曲线位图(Custom 模式除外),并同步更新当前值键 |
getCacheEqInfo(int modeIndex) | EqInfo | 读取指定模式缓存;无缓存时返回 EQ_VALUES[modeIndex] 默认值 |
getEqInfoString(EqInfo / int modeIndex) | String | 读取指定模式的 JSON 缓存字符串(空串表示无缓存) |
getEqValueBitMapPath(EqInfo eqInfo) | String | 返回曲线位图路径;文件不存在时自动绘制并保存 |
getCurrentCacheEqInfo() | EqInfo | 读取当前生效 EQ;无缓存返回 Normal(mode=0,全零) |
getPresetEqInfo() | EqPresetInfo | 读取预设 EQ 信息;无缓存时用 EQ_VALUES 构造 7 模式默认预设 |
savePresetEqInfo(EqPresetInfo eqPresetInfo) | void | 将预设 EQ 信息序列化为 JSON 持久化 |
createBitmap(EqInfo eqInfo) | Bitmap | 用 EQPlotCore+EqCovertUtil 绘制 250×50dp 的 EQ 曲线位图 |
clear() | void | 清除当前值键与预设值键(保留各模式键) |
EqInfo(com.jieli.bluetooth.bean.device.eq.EqInfo,SDK 类型)
| 字段/方法 | 类型 | 说明 |
|---|---|---|
getMode() / setMode(int) | int | 模式索引,0~6,对应 EQ_VALUES 下标 |
getValue() / setValue(byte[]) | byte[] | 10 段增益值(有符号 dB) |
getFreqs() / setFreqs(int[]) | int[] | 10 个频率点(Hz),默认来自 BluetoothConstant.DEFAULT_EQ_FREQS |
new EqInfo(mode, byte[]) | 构造器 | 以指定模式与增益数组构造 |
BluetoothHelper(com.jieli.btsmart.tool.bluetooth.BluetoothHelper)
| 方法 | 说明 |
|---|---|
getEqInfo(CommandCallback) | 构造 buildGetEqValueCmd() 并发送查询命令,结果经 GetSysCommandCallback 分发 |
RCSPController(SDK 单例,签名以 SDK 为准,参见 EqDemo 用法)
getEqInfo(BluetoothDevice device, OnRcspActionCallback<Boolean> callback)— 查询 EQ 信息,数据经BTRcspEventCallback.onEqPresetChange/onEqChange回调configEqInfo(BluetoothDevice device, EqInfo eqInfo, OnRcspActionCallback<Boolean> callback)— 配置 EQgetExpandDataInfo(BluetoothDevice device, OnRcspActionCallback<Boolean> callback)— 获取扩展音效信息,经onExpandFunction回调setExpandDataInfo(BluetoothDevice device, int mask, byte[] data, OnRcspActionCallback<Boolean> callback)— 按掩码写入扩展音效参数setReverberationParameter(BluetoothDevice device, ReverberationParam param, OnRcspActionCallback<Boolean> callback)— 设置混响setDynamicLimiterParameter(BluetoothDevice device, DynamicLimiterParam param, OnRcspActionCallback<Boolean> callback)— 设置动态限幅
回调语义:OnRcspActionCallback<Boolean> 的 onSuccess(device, message) 表示命令执行成功、onError(device, BaseError) 携带错误信息;实际数据以 BTRcspEventCallback 事件为准。
Configuration Options
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
EQ_VALUES[0..6] | byte[][] | 见预设表 | 7 种模式的出厂默认增益值,常量不可修改 |
KEY_EQ_VALUE | String | "KEY_EQ_VALUE" | 按模式缓存键前缀,实际键为 KEY_EQ_VALUE + mode |
KEY_EQ_CURRENT_VALUE | String | "KEY_EQ_CURRENT_VALUE" | 当前生效 EQ 缓存键 |
KEY_EQ_PRESET | String | "KEY_EQ_PRESET" | 预设 EQ 信息缓存键 |
DEFAULT_EQ_FREQS | int[] | SDK 定义 | 默认 10 个频率点,来自 BluetoothConstant |
getStatusKey() 设备后缀 | String | 无(已注释) | 预留的按设备地址隔离缓存键逻辑 |
启动时 EqCacheUtil.clear() | boolean | 启用 | MainApplication.init() 每次冷启动重置 EQ 缓存 |
Failure Modes、边界与并发
失败模式
- 命令失败:
configEqInfo/getEqInfo失败时回调onError(device, BaseError),界面应提示用户并保持上次已知状态;缓存仍保留,不会因一次失败而丢失用户配置; - 设备未连接/断连:
RCSPController.getUsingDevice()可能为空或已失联,SDK 层返回错误;UI 依赖本地缓存仍可展示"上次配置",但应禁用下发交互; - 位图生成失败:
createBitmapAndSave依赖getExternalCacheDir(),若外部存储不可用(如被卸载/只读),文件写入异常未被显式捕获——接入方在调用getEqValueBitMapPath时应自行兜底(如直接使用createBitmap绘制到内存); - 缓存数据损坏:
string2eqInfo直接用 Gson 反序列化,若 SharedPreferences 中的 JSON 被破坏会抛异常;当前实现未做 try-catch,属已知薄弱点。
边界与一致性
- Custom 模式特判:
saveEqValue对mode == 6不删除/生成位图,因为自定义曲线走实时绘制,避免每次拖动都触发文件系统操作; - 值比较用 JSON 字符串:
saveEqValue以eqInfo2String的 JSON 相等性判断"是否变化",字段顺序由 Gson 固定,可靠且实现简单; - 并发安全:
EqCacheUtil为静态方法 + SharedPreferences,Android 多进程场景下无锁;单进程内主线程读写与异步回调写入可能交错,但 SharedPreferences 内部有锁保护,最坏情况是"最后一次写入生效",可接受; - 静态数组别名风险:
EQ_VALUES是public static常量,getPresetEqInfo使用clone()拷贝正是为了防止外部直接修改默认值污染全局。
性能与运维考量
- 内容寻址位图缓存:位图文件名 =
eq=<hex增益> + <频率点拼接>,同一 EQ 配置必然命中同一路径,重复渲染零成本;外部缓存目录文件可被系统自动清理,无需手动维护; - 懒生成:位图仅在首次需要时生成(
file.exists()检查),并随 EQ 值变化由saveEqValue主动删除失效,避免陈旧曲线; - 启动开销:
MainApplication.init()调用EqCacheUtil.clear()仅为两次remove,成本可忽略;EQ 查询在进入界面后才发起,不阻塞启动; - 绘制成本:曲线位图固定 250×50dp、
RGB_565,单次绘制仅 1 次drawLines,适合列表复用(Adapter 直接取路径加载,不再逐项重绘)。
Extension Points
- 按设备隔离 EQ 记忆:
getStatusKey()中被注释的key + "-" + device.getAddress()逻辑是官方预留的扩展点,放开后即可实现"不同设备各自记忆 EQ 配置"; - 自定义模式扩展:
EQ_VALUES数组新增条目即可扩展预设模式数量,但需同步调整getPresetEqInfo()的setNumber(7)循环上界与 UI 模式列表; - 扩展音效参数:
setExpandDataInfo(mask, data)的 mask 机制允许按位扩展新音效类型,ReverberationParam/DynamicLimiterParam之外的参数结构可沿用同一通道; - 曲线渲染替换:
createBitmap中EQPlotCore+EqCovertUtil可整体替换为自定义绘图实现,getEqValueBitMapPath的缓存契约不变。
Tests
官方在 btsmart/src/test/java/com/jieli/btsmart/demo/EqDemo.java 中提供了 EQ 功能的集成演示(非单元断言),覆盖六类操作:getEqInfo、configEqInfo、getExpandDataInfo、setExpandDataInfo、setReverberationParameter、setDynamicLimiterParameter。该文件同时是 API 用法的权威参考——所有 RCSPController EQ 方法签名均以此为准。EqCacheUtil 的纯逻辑(JSON 序列化、位图路径生成)未见独立单元测试,建议接入方补充。
Related Links
- EqCacheUtil.java(缓存与渲染核心)
- EqDemo.java(官方 EQ API 演示)
- BluetoothHelper.java(指令通道门面)
- MainApplication.java(EQ 缓存生命周期)
- SDK 类型参考(
EqInfo/EqPresetInfo/ReverberationParam/DynamicLimiterParam):com.jieli.bluetooth.bean.device.eq包,位于 SDK 依赖库中 - 相关目录页:音效模式调节(
AdjustVoiceMode)、设备设置页导航、蓝牙连接管理