杰理 SDK 文档中心
首页
首页
  • 快速开始

    • 环境要求与权限配置
    • 集成SDK依赖
    • 运行示例应用
  • 核心架构与协议

    • RCSP协议与数据通道
    • 蓝牙连接与设备管理
    • TWS双耳功能
    • 基础功能接口与自定义命令
  • 设备功能控制

    • 设备音乐控制与ID3信息
    • 文件浏览与传输
    • FM收音与发射
    • 灯光控制
    • 闹钟与时间管理
    • 查找设备与防丢
    • ANC与噪声处理
    • 按键功能设置
    • 彩屏仓控制
    • AI翻译
  • 音效与音频处理

    • 均衡器音效调节
    • 录音与语音控制
    • Line-in、SPDIF与声卡功能
    • 音频编解码库
  • 扩展功能库

    • OTA固件升级
    • 数据加密与解密
    • 图片与动图格式转换
  • 示例应用 btsmart

    • 应用架构与界面导航
    • 设备功能适配与数据层
    • 设备配置JSON与资源文件
  • 参考与版本

    • 错误码参考
    • 版本历史与更新日志
    • 开发文档中心导航

均衡器音效调节

均衡器(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

在杰理蓝牙方案中,均衡器调节本质上是一个「应用层 ↔ 蓝牙设备」的双向参数同步过程:

  1. 指令通道:App 通过 RCSPController 向已连接的蓝牙设备(耳机/音箱)发送 EQ 查询或配置命令,设备端 DSP 据此调整音频频响曲线;
  2. 本地缓存:为避免每次启动都重新拉取、也为了在设备断连后保留用户偏好,App 使用 EqCacheUtil 将各模式的增益值序列化为 JSON 存入 SharedPreferences;
  3. 可视化: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};

来源:EqCacheUtil.java

设计要点:

  • 模式索引即数组下标: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(正常)00, 0, 0, 0, 0, 0, 0, 0, 0, 0
Rock(摇滚)1-2, 0, 2, 4, -2, -2, 0, 0, 4, 4
Pop(流行)23, 1, 0, -2, -4, -4, -2, 0, 1, 2
Classical(古典)30, 8, 8, 4, 0, 0, 0, 0, 2, 2
Jazz(爵士)40, 0, 0, 4, 4, 4, 0, 2, 3, 4
Country(乡村)5-2, 0, 0, 2, 2, 0, 0, 0, 4, 4
Custom(自定义)60, 0, 0, 0, 0, 0, 0, 0, 0, 0

EqCacheUtil 缓存机制

EqCacheUtil 是一个纯静态工具类(无实例状态),围绕三条 SharedPreferences 键组织缓存:

键前缀用途内容格式
KEY_EQ_VALUE("KEY_EQ_VALUE")按模式保存的 EQ 值,键为 KEY_EQ_VALUE + modeEqInfo 的 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);
}

来源:EqCacheUtil.java

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);
}

来源:EqCacheUtil.java

兜底逻辑:缓存为空(首次启动或已被清除)时,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);
}

来源:EqCacheUtil.java

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));
}

来源:EqCacheUtil.java

clear() 只清理"当前值"与"预设值",保留各模式的 KEY_EQ_VALUE + mode 缓存。MainApplication.init() 在每次 App 启动时调用 EqCacheUtil.clear()(注释说明"暂时重新打开 app 时重置 eq 缓存,如有需要可以放开"),即当前策略是每次冷启动重置 EQ 状态,避免设备切换后残留旧设备的 EQ 配置。

来源:MainApplication.java

另外注意 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);
}

来源:EqCacheUtil.java

路径生成规则把"内容哈希"直接编码进文件名,天然实现内容寻址式缓存:

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();
}

来源:EqCacheUtil.java

曲线绘制采用两段式管线:

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;
}

来源:EqCacheUtil.java

渲染细节与设计取舍:

  • 固定画布尺寸: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));
}

来源:BluetoothHelper.java

该实现体现了两层设计:

  • CommandBuilder.buildGetEqValueCmd() 负责把"查询 EQ 值"这一语义编译成 RCSP 协议字节流;
  • GetSysCommandCallback(mCallbackManager, commandCallback) 把 SDK 的系统命令回调统一桥接到 mCallbackManager,业务方只需关心 CommandCallback,无需理解协议细节。

BluetoothHelper 在更新设备信息(如重连后恢复状态)时也会同步保留 EQ 相关字段:

.setEqInfo(oldDevInfo.getEqInfo())
.setEqPresetInfo(oldDevInfo.getEqPresetInfo())

来源:BluetoothHelper.java

这意味着设备信息对象(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() 更新缓存

流程说明:

  1. 界面先读本地缓存立即渲染(首帧不依赖蓝牙往返,减少白屏);
  2. 同时通过 BluetoothHelper.getEqInfo() 向设备发起查询;
  3. 设备回传后,SDK 触发 BTRcspEventCallback.onEqPresetChange 与 onEqChange 两个事件;
  4. 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);

来源:EqCacheUtil.java

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) — 配置 EQ
  • getExpandDataInfo(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_VALUEString"KEY_EQ_VALUE"按模式缓存键前缀,实际键为 KEY_EQ_VALUE + mode
KEY_EQ_CURRENT_VALUEString"KEY_EQ_CURRENT_VALUE"当前生效 EQ 缓存键
KEY_EQ_PRESETString"KEY_EQ_PRESET"预设 EQ 信息缓存键
DEFAULT_EQ_FREQSint[]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)、设备设置页导航、蓝牙连接管理
Next
录音与语音控制