音量与音频输出管理
BleVolumeManager / BleVolumeProcessor 与 VolumeInfo / VolumeCtrlInfo 构成了 JieLi Home Flutter SDK 中「音量与音频输出」能力的完整闭环:业务层通过 MethodChannel 向原生 BLE SDK 下发查询/设置指令,原生 SDK 将设备返回的音量状态以事件形式回传,Flutter 侧统一解析为数据模型并暴露为响应式 Stream。
Purpose and Scope
本页聚焦 音量(Volume)与高低音(Treble/Bass,即音频输出控制) 这条完整能力链路,覆盖:
- 命令下发入口
BleVolumeManager(查询当前音量、设置音量/高音/低音); - 事件接收入口
BleVolumeProcessor(音量变化事件、高低音变化事件的 Stream); - 数据模型
VolumeInfo与VolumeCtrlInfo及其序列化约定; - Flutter 与原生 BLE SDK(Android
VolumeManager.kt、iOSVolumeManager.swift)之间的桥接边界; - 相关方法常量与事件常量、失败模式、并发与扩展方式。
本页不包含:蓝牙连接/设备扫描、播放控制(播放/暂停/切歌)、EQ 均衡器等其他音乐媒体能力——这些属于 4-music-media 目录下的兄弟页面,本页仅在涉及事件通道时引用公共的 BleBaseManager / BleBaseEventProcessor。
概述
在 JieLi(杰理)蓝牙生态中,音量与音频输出能力横跨三层:
- Flutter 应用层:业务页面调用
BleVolumeManager的静态方法下发指令,订阅BleVolumeProcessor暴露的 Stream 获得设备状态变化; - 桥接层:
BleBaseManager.invokeMethod通过 MethodChannel 把指令与参数序列化后交给原生侧;BleBaseEventProcessor是全局事件总线,负责把原生回调分发到各类型事件的订阅者; - 原生 BLE SDK 层:Android 侧
VolumeManager.kt/VolumeInfo.java/VolumeCtrl.java,iOS 侧VolumeManager.swift/VolumeEventProcessor.swift(底层为JL_SystemVolume协议),负责与蓝牙设备(耳机/音箱)实际通信。
设计上,BleVolumeManager 与 BleVolumeProcessor 均为纯静态类、无内部状态:命令只是一次异步 MethodChannel 调用,事件则是被 BleBaseEventProcessor.filterByType 过滤后的冷流映射。这种「命令-事件」分离的设计让音量能力天然线程安全、可组合,业务方只需关心「发什么指令」和「听什么事件」,无需感知设备协议细节。
架构
flowchart TD
subgraph sg_UI["Flutter 应用层"]
Page["业务页面 / Example 页面"]
end
subgraph sg_Command["命令下发(静态方法)"]
Mgr["BleVolumeManager"]
BaseMgr["BleBaseManager.invokeMethod"]
MConst["BleMethodConstants"]
end
subgraph sg_Event["事件接收(响应式 Stream)"]
BaseProc["BleBaseEventProcessor(事件总线)"]
Proc["BleVolumeProcessor"]
EConst["BleEventConstants"]
end
subgraph sg_Model["数据模型"]
VI["VolumeInfo<br/>maxVol / volume / supportVolumeSync"]
VCI["VolumeCtrlInfo<br/>high / bass"]
end
subgraph sg_Native["原生桥接(MethodChannel)"]
Android["VolumeManager.kt<br/>VolumeInfo.java / VolumeCtrl.java"]
IOS["VolumeManager.swift<br/>VolumeEventProcessor.swift / JL_SystemVolume"]
end
subgraph sg_Device["蓝牙设备"]
Dev["耳机 / 音箱(BLE)"]
end
Page -->|"getCurrentVolume / setCurrentVolume"| Mgr
Mgr --> BaseMgr
Mgr --> MConst
BaseMgr -->|"MethodChannel 调用"| Android
BaseMgr -->|"MethodChannel 调用"| IOS
Android -->|"BLE 指令"| Dev
IOS -->|"BLE 指令"| Dev
Dev -->|"音量/高低音状态回包"| Android
Dev -->|"音量/高低音状态回包"| IOS
Android -->|"原生事件回调"| BaseProc
IOS -->|"原生事件回调"| BaseProc
BaseProc -->|"filterByType 过滤"| Proc
Proc -->|"VolumeInfo.fromMap"| VI
Proc -->|"VolumeCtrlInfo.fromMap"| VCI
VI --> Page
VCI --> Page
各角色职责
| 组件 | 职责 | 状态 |
|---|---|---|
BleVolumeManager | 音量指令下发入口,静态方法封装 MethodChannel 调用 | 无状态 |
BleVolumeProcessor | 音量/高低音事件订阅入口,把事件总线消息映射为模型 Stream | 无状态 |
VolumeInfo | 音量快照:最大音量、当前音量、是否支持音量同步 | 纯数据 |
VolumeCtrlInfo | 音频输出控制快照:高音(treble)、低音(bass) | 纯数据 |
BleBaseManager / BleBaseEventProcessor | 公共桥接基座:MethodChannel 调用与全局事件分发(属于 SDK 公共层,详见连接管理页面) | 全局单例/静态 |
设计意图:把「命令」与「事件」拆成两个类,是因为两者生命周期完全不同——指令是一次性 fire-and-forget 的异步调用,事件则可能是设备在任意时刻主动上报(例如用户在耳机上按键调音量)。若合并为一个类,静态无状态的设计会被破坏,订阅方与调用方也会耦合。
核心组件详解
1. 命令下发:BleVolumeManager
BleVolumeManager 是音量能力唯一的指令入口,全部方法为 static,内部只做一件事:把参数打包成 Map,转交 BleBaseManager.invokeMethod 走 MethodChannel 下发到原生 SDK。
/// Volume Manager
class BleVolumeManager {
static Future<void> getCurrentVolume() async {
await BleBaseManager.invokeMethod(
BleMethodConstants.methodGetCurrentVolume,
);
}
static Future<void> setCurrentVolume({
required int currentVolume,
required int treble,
required int bass,
}) async {
await BleBaseManager.invokeMethod(
BleMethodConstants.methodSetVolume,
arguments: {
BleMethodConstants.argCurrentVol: currentVolume,
BleMethodConstants.argTreble: treble,
BleMethodConstants.argBass: bass
},
);
}
}
Source: ble_volume_manager.dart
要点分析:
getCurrentVolume():无参查询,方法名取自BleMethodConstants.methodGetCurrentVolume。查询结果不通过返回值返回,而是由设备随后上报typeVolumeChanged事件——这是典型的异步「指令-事件」配对模式:查询指令发出后,订阅方等待对应事件即可。setCurrentVolume({currentVolume, treble, bass}):一次调用同时设置当前音量、高音、低音三个参数,说明设备侧的音量指令是一个复合指令(音量与音频输出控制共用一条指令通道)。三个参数均为required,Dart 编译器层面强制调用方显式传入,避免遗漏某个频段导致设备端状态不一致。- 方法返回
Future<void>且await了invokeMethod,即只等待「指令已送达原生层」,并不等待设备确认——设备确认仍以事件形式异步返回。这也是为什么该 API 无需透传错误码。
2. 事件接收:BleVolumeProcessor
BleVolumeProcessor 是音量事件的唯一订阅入口。它复用公共事件总线 BleBaseEventProcessor.filterByType,按事件类型过滤后把原始 Map 映射为强类型模型,再以 Stream 暴露给业务方。
/// Volume event processor
class BleVolumeProcessor {
static Stream<VolumeInfo> get volumeInfoStream {
return BleBaseEventProcessor.filterByType(
BleEventConstants.typeVolumeChanged,
).map((event) {
final data = BleBaseEventProcessor.getValueFromEvent(event);
return VolumeInfo.fromMap({
BleEventConstants.keyMaxVol:
data[BleEventConstants.keyMaxVol] as int? ?? 0,
BleEventConstants.keyCurrentVol:
data[BleEventConstants.keyCurrentVol] as int? ?? 0,
BleEventConstants.keySupportVolumeSync:
data[BleEventConstants.keySupportVolumeSync] as bool? ?? false,
});
});
}
static Stream<VolumeCtrlInfo> get volumeCtrlStream {
return BleBaseEventProcessor.filterByType(
BleEventConstants.typeHeightBassChanged,
).map((event) {
final data = BleBaseEventProcessor.getValueFromEvent(event);
return VolumeCtrlInfo.fromMap({
BleEventConstants.keyTreble:
data[BleEventConstants.keyTreble] as int? ?? 0,
BleEventConstants.keyBass:
data[BleEventConstants.keyBass] as int? ?? 0
});
});
}
}
Source: ble_volume_processor.dart
要点分析:
- 两个事件类型对应两条 Stream:
typeVolumeChanged→volumeInfoStream(音量状态);typeHeightBassChanged→volumeCtrlStream(高低音状态)。设备端把「音量」与「高低音」作为两个独立事件上报,因此消费方可以分别订阅,互不阻塞。 - 解析防御:
as int? ?? 0、as bool? ?? false表明原生侧字段可能缺失或类型不符,解析层兜底为默认值,保证 Stream 永不因单字段异常而中断——这是跨语言桥接中常见的容错设计。 - 映射职责独立:processor 只负责「取数据 + 转模型」,不持有任何 UI 状态,因此多个页面可以同时订阅同一 Stream 而互不干扰(
filterByType返回的流可被多路监听)。
3. 数据模型
3.1 VolumeInfo —— 音量快照
/// Volume info model
class VolumeInfo {
final int maxVol; // 最大音量
final int volume; // 当前音量
final bool supportVolumeSync; // 是否支持音量同步
VolumeInfo({
required this.maxVol,
required this.volume,
required this.supportVolumeSync,
});
factory VolumeInfo.fromMap(Map<String, dynamic> map) {
return VolumeInfo(
maxVol: map[BleEventConstants.keyMaxVol] ?? 0,
volume: map[BleEventConstants.keyCurrentVol] ?? 0,
supportVolumeSync: map[BleEventConstants.keySupportVolumeSync] ?? false,
);
}
Map<String, dynamic> toMap() {
return {
BleEventConstants.keyMaxVol: maxVol,
BleEventConstants.keyCurrentVol: volume,
BleEventConstants.keySupportVolumeSync: supportVolumeSync,
};
}
}
Source: volume_info.dart
supportVolumeSync 是音量同步能力的标志位:部分设备支持「手机调音量 ↔ 设备实际音量」双向同步,业务方可据此决定是否在 UI 上显示联动提示或禁用本地滑杆。fromMap 与 toMap 成对出现,保证同一模型既能从原生事件反序列化,也能被序列化回传(例如用于状态持久化或日志)。
3.2 VolumeCtrlInfo —— 音频输出控制快照
/// Volume ctrl model
class VolumeCtrlInfo {
final int high; // 高音
final int bass; // 低音
VolumeCtrlInfo({
required this.high,
required this.bass
});
factory VolumeCtrlInfo.fromMap(Map<String, dynamic> map) {
return VolumeCtrlInfo(
high: map[BleEventConstants.keyTreble] ?? 0,
bass: map[BleEventConstants.keyBass] ?? 0
);
}
Map<String, dynamic> toMap() {
return {
BleEventConstants.keyTreble: high,
BleEventConstants.keyBass: bass
};
}
}
Source: volume_ctrl_info.dart
注意 VolumeCtrlInfo.high 在序列化时使用的 key 是 keyTreble(高音),即模型字段名与协议字段名并不一一对应:Dart 侧用更直观的 high,协议层用 treble。这种命名隔离说明模型是面向业务语义设计的,协议细节被封装在常量层。
4. 常量约定
方法名、参数 key、事件类型、事件字段 key 全部集中在 BleMethodConstants(ble_method_constants.dart)与 BleEventConstants(ble_event_constants.dart)两个常量类中,本页涉及的常量如下(名称取自上述源码中的实际引用):
| 常量 | 值语义 | 所属常量类 | 使用位置 |
|---|---|---|---|
methodGetCurrentVolume | 查询当前音量指令 | BleMethodConstants | getCurrentVolume() |
methodSetVolume | 设置音量/高音/低音指令 | BleMethodConstants | setCurrentVolume() |
argCurrentVol | 指令参数:当前音量 | BleMethodConstants | setCurrentVolume() |
argTreble | 指令参数:高音 | BleMethodConstants | setCurrentVolume() |
argBass | 指令参数:低音 | BleMethodConstants | setCurrentVolume() |
typeVolumeChanged | 音量变化事件类型 | BleEventConstants | volumeInfoStream 过滤 |
typeHeightBassChanged | 高低音变化事件类型 | BleEventConstants | volumeCtrlStream 过滤 |
keyMaxVol | 事件字段:最大音量 | BleEventConstants | VolumeInfo.fromMap |
keyCurrentVol | 事件字段:当前音量 | BleEventConstants | VolumeInfo.fromMap |
keySupportVolumeSync | 事件字段:是否支持音量同步 | BleEventConstants | VolumeInfo.fromMap |
keyTreble | 事件字段:高音 | BleEventConstants | VolumeCtrlInfo.fromMap |
keyBass | 事件字段:低音 | BleEventConstants | VolumeCtrlInfo.fromMap |
统一常量层的价值在于:原生与 Flutter 之间以字符串协议通信,任何一处硬编码字符串都可能导致「指令发了但事件收不到」的静默故障;集中定义后,IDE 重构与编译期检查可以覆盖全部引用点。
核心流程
指令-事件闭环(设置音量)
sequenceDiagram
participant UI as 业务页面
participant Mgr as BleVolumeManager
participant Base as BleBaseManager<br/>(MethodChannel)
participant Native as 原生 BLE SDK<br/>(Android / iOS)
participant Dev as 蓝牙设备
participant Bus as BleBaseEventProcessor<br/>(事件总线)
participant Proc as BleVolumeProcessor
participant Model as VolumeInfo / VolumeCtrlInfo
UI->>Mgr: setCurrentVolume(currentVolume, treble, bass)
activate Mgr
Mgr->>Base: invokeMethod(methodSetVolume,<br/>{argCurrentVol, argTreble, argBass})
Base->>Native: MethodChannel 调用
Native->>Dev: 下发 BLE 音量设置指令
Dev-->>Native: 音量状态回包
Native-->>Bus: 原生事件回调<br/>(volumeChanged / heightBassChanged)
deactivate Mgr
Bus->>Proc: filterByType 过滤事件
Proc->>Model: fromMap 解析
Model-->>UI: Stream 推送<br/>(volumeInfoStream / volumeCtrlStream)
UI->>UI: 更新音量滑杆 / 高低音显示
流程关键点
- 指令方向是「调用链」:UI →
BleVolumeManager→BleBaseManager.invokeMethod→ MethodChannel → 原生 SDK → 设备。每一步都是同步 await 的异步调用,但不等待设备回包。 - 事件方向是「事件总线」:设备回包 → 原生 SDK 回调 →
BleBaseEventProcessor统一入总线 →BleVolumeProcessor按filterByType过滤 →.map转模型 → Stream 推送 UI。两条方向完全解耦,这也是为什么查询音量时getCurrentVolume()的返回值是Future<void>——查询结果永远通过事件流回来。 - 查询与设置的配对:业务方执行「查询 → 订阅流 → 等首个事件」是标准用法;若设备支持音量同步(
supportVolumeSync == true),后续用户在耳机侧调节音量也会触发typeVolumeChanged,UI 无需主动轮询。
使用示例
基本用法:查询并监听音量状态
// 1. 先订阅事件流(防止错过设备回包)
BleVolumeProcessor.volumeInfoStream.listen((info) {
// info.maxVol 最大音量 / info.volume 当前音量 / info.supportVolumeSync 是否支持同步
print('max=${info.maxVol}, current=${info.volume}, sync=${info.supportVolumeSync}');
});
// 2. 再下发查询指令
await BleVolumeManager.getCurrentVolume();
高级用法:设置音量与高低音、订阅音频输出控制
// 订阅高低音变化事件
BleVolumeProcessor.volumeCtrlStream.listen((ctrl) {
// ctrl.high 高音 / ctrl.bass 低音
print('high=${ctrl.high}, bass=${ctrl.bass}');
});
// 一次调用同时设置当前音量、高音、低音
await BleVolumeManager.setCurrentVolume(
currentVolume: 80,
treble: 10,
bass: 6,
);
提示:仓库中的示例工程
code/JieLi_Home_Demo/example/lib/pages/volume_page.dart提供了完整的音量设置页面 Demo(滑杆 + 高低音调节),可直接作为业务接入参考。
API 参考
BleVolumeManager
| 方法签名 | 说明 | 参数 | 返回 | 异常 |
|---|---|---|---|---|
static Future<void> getCurrentVolume() | 下发查询当前音量指令;结果通过 volumeInfoStream 事件返回 | 无 | 仅表示指令已送达原生层 | 由 BleBaseManager.invokeMethod 抛出的平台通道异常 |
static Future<void> setCurrentVolume({required int currentVolume, required int treble, required int bass}) | 同时设置当前音量、高音、低音 | currentVolume:当前音量(0~maxVol);treble:高音;bass:低音 | 仅表示指令已送达原生层 | 同上;参数为 required,缺失会在编译期报错 |
BleVolumeProcessor
| 属性 | 类型 | 说明 |
|---|---|---|
static Stream<VolumeInfo> get volumeInfoStream | Stream<VolumeInfo> | 音量变化事件流;每次设备上报音量状态推出一条 VolumeInfo |
static Stream<VolumeCtrlInfo> get volumeCtrlStream | Stream<VolumeCtrlInfo> | 高低音变化事件流;每次设备上报推出一条 VolumeCtrlInfo |
数据模型
| 类 | 构造 | 关键方法 | 字段 |
|---|---|---|---|
VolumeInfo | VolumeInfo({required maxVol, required volume, required supportVolumeSync}) | factory fromMap(Map) / Map toMap() | int maxVol、int volume、bool supportVolumeSync |
VolumeCtrlInfo | VolumeCtrlInfo({required high, required bass}) | factory fromMap(Map) / Map toMap() | int high(协议 key 为 treble)、int bass |
失败模式与边界情况
- 字段缺失/类型不符兜底:
VolumeInfo.fromMap与VolumeCtrlInfo.fromMap对每个字段使用?? 0/?? false兜底,BleVolumeProcessor内还额外做了as int?/as bool?强转。含义是:即使原生侧事件数据不完整,Stream 仍会推送一条「默认值模型」,业务侧不应假设模型字段必然有效,尤其是volume == 0并不代表设备静音,需结合maxVol判断。 - 先订阅后下发:由于查询结果只通过事件流返回,若业务方先调
getCurrentVolume()再订阅volumeInfoStream,可能错过回包事件导致 UI 不更新。正确顺序是先listen再发指令(见使用示例)。 - 指令 Fire-and-Forget 的语义:
setCurrentVolume只 await 到「指令送达原生层」,设备是否真正生效、参数是否越界(如音量超过maxVol)均不通过返回值反馈,业务方只能依赖后续事件或设备行为自行校验。 - 设备侧主动调音量:用户直接在耳机/音箱上按键调音量时,设备会主动上报
typeVolumeChanged。若supportVolumeSync == false,UI 上应避免用本地滑杆值覆盖设备值或反向猜测设备状态,否则会出现滑杆与真实音量漂移。 - 断连/未连接场景:本页 API 不校验蓝牙连接状态,指令会直接走
BleBaseManager.invokeMethod;未连接设备时由原生层决定忽略或抛平台异常(具体行为取决于BleBaseManager与原生 SDK 的实现)。
并发与性能
- 静态无状态设计:
BleVolumeManager/BleVolumeProcessor不含可变成员,多个页面并发调用或订阅不会产生共享状态竞争,天然线程安全。 - 冷流多订阅:
filterByType派生出的 Stream 可被多个监听者同时订阅,每个订阅者独立收到事件;处理器本身不做缓存或重放,晚订阅者不会收到历史事件——需要「最近一次音量状态」的业务方应自行缓存。 - 事件频率:音量变化事件可能由设备高频上报(如长按音量键连续调节)。processor 的解析逻辑为 O(1) 的 Map 读取,开销可忽略;若 UI 侧需要节流(如滑杆动画),建议在业务层使用
debounce,本页代码未内置该机制。
扩展点
- 新增音量相关事件:在
BleEventConstants中新增事件类型常量,然后在BleVolumeProcessor中仿照volumeInfoStream增加一条filterByType(新类型).map(...)的 Stream getter 即可;事件总线与桥接层无需任何改动。 - 新增指令:在
BleMethodConstants中新增方法名与参数 key,在BleVolumeManager中新增静态方法调用BleBaseManager.invokeMethod,并同步在 Android/iOS 原生 SDK 的 Volume Manager 中注册对应方法处理。 - 自定义解析:若业务需要原始 Map 而非强类型模型,可直接订阅
BleBaseEventProcessor.filterByType(...)拿原始事件数据,processor 的模型映射不是必经之路。 - 原生侧扩展:Android 侧可参考
code/JieLi_Home_Demo/android/src/main/kotlin/com/jieli/bt/sdk/data/manager/VolumeManager.kt及模型VolumeInfo.java/VolumeCtrl.java;iOS 侧可参考code/JieLi_Home_Demo/ios/Classes/Manager/VolumeManager.swift、VolumeEventProcessor.swift与JL_SystemVolume.h。两边桥接类均与本页 Flutter 类同名同职责,扩展时需保持方法名与参数 key 一致。
相关链接
- 公共桥接基座:
BleBaseManager/BleBaseEventProcessor(连接管理、事件总线,见 4-music-media 目录下连接相关页面) - 示例页面:volume_page.dart(音量设置完整 Demo)
- 命令下发:ble_volume_manager.dart
- 事件处理:ble_volume_processor.dart
- 数据模型:volume_info.dart、volume_ctrl_info.dart
- 原生桥接:Android VolumeManager.kt / iOS VolumeManager.swift