概述

LE Audio整体框图如下,对于BlueZ协议栈来说主要是实现了Generic Audio Framework层的profile,本章节主要针对BAP介绍CIS和BIS的相关service。 BlueZ协议栈的介绍可以参考Linux蓝牙框架(三) - BlueZ协议栈 链接。

LE Audio Profile

从上图可知除了BAP,还有其他Profile,例如MCP(Media Control Profile), CCP(Call Control Profile), CSIP (Coordinated Set Identification Profile), MICP(Microphone Control Profile), VCP(Volume Control Profile),本章节的主要介绍BAP以及BAP下的基础service(PACS/ASCS/BASS);

BAP SPEC中定义了如下角色,下面的章节将针对Unicast和Broadcast分别介绍.

Role Sub Role Description
Unicast Unicast Server 发送广播
声明支持的audio capabilities
声明ASEs(Audio Stream Endpoint)
通知当前音频流的可用性状态
接受/拒绝CIS建立请求,结束CIS连接音频服务
Unicast Client 扫描广播,发起LE连接
发现和配置Unicast Server 声明的服务以及音频流能力/状态
发起和结束CIG/CIS连接
Broadcast Broadcast Source 配置BIG/BIS,并声明公共音频配置信息(PAC)
发送音频流数据
Broadcast Sink 发现Broadcast Source的音频配置信息
接收Audio stream数据
声明音频能力 (用于Assistant配置)
Broadcast Assistant 发现Broadcast Sink的音频能力
发现Broadcast Source设备和Audio stream 配置
将Broadcast Source设备相关信息配置到Broadcast Sink设备上
接收Broadcast Sink设备的音频流状态变化
Scan Delegator 协助扫描Broadcast Source设备
获取加密Broadcast Code

BlueZ软件框图

BlueZ SW Block Diagram

BAP in BlueZ

BAP在BlueZ中分为两个部分src/shared/bap.c和profiles/audio/bap.c,这两部分在BlueZ框架中有明确的功能划分

BAP核心逻辑和底层行为 (src/shared/bap.c),主要功能如下:

  • 定义 BAP 的核心数据结构
    • 定义 BAP Stream、Endpoint、Codec、QoS 等结构体。例如:struct bt_bap_stream; struct bt_bap_qos; struct bt_bap_endpoint;
  • 这些对象与 ASCS/GATT 紧密绑定
  • 处理 GATT 层交互
    • 实现 BAP 在 GATT 层的发现、配置、通知逻辑。包括:
      • 发现 ASE(Audio Stream Endpoint)
      • 设置 Codec Configuration
      • 配置 QoS
      • 启用 / 禁用 Stream
  • 抽象 BAP Stream 生命周期管理
    • BAP 是 ASCS 的上层逻辑封装,负责把多个 ASE 组织为一个 stream。
      提供接口
      • bt_bap_stream_config()
      • bt_bap_stream_qos()
      • bt_bap_stream_enable()
      • bt_bap_stream_start()
      • bt_bap_stream_disable()
      • bt_bap_stream_stop()
      • bt_bap_stream_metadata()
      • bt_bap_stream_release()
  • 维护本地与远端 ASE 的状态同步
    • 当手机端发起操作(如 enable),shared 层更新 stream 状态并通过回调通知上层。
  • 不暴露 D-Bus 接口,它是纯逻辑层,不直接面向应用层。

BAP Profile 逻辑与 D-Bus 集成层 (profiles/audio/bap.c),该文件是真正注册为 BlueZ “BAP Profile” 的实现部分,它负责把 shared/bap.c 的底层协议封装成可被外部系统控制的 DBus 接口。
主要功能:

  • 注册 BAP Profile
    • 调用 BlueZ Profile 注册框架(audio_register_profile())。
    • 初始化并管理多个 BAP 实例(unicast client/server)。
    • 例如:static struct bap_data *bap_register(struct btd_adapter *adapter, bool unicast);
  • 创建和管理 D-Bus 对象
    • 把每个 transport、endpoint 映射成 D-Bus 对象(例如object path /MediaEndpointLE/BAPSink/lc3)。
    • 例如暴露Endpoint的方法SetConfiguration,这些方法由 PipeWire 或 Android Audio Service 调用。
  • 调用 shared 层 API
    • 当上层通过 D-Bus 发起命令时(例如配置一个新的 stream),profiles/audio/bap.c 会调用 shared 层接口来执行,例如:bap_config() -> bt_bap_stream_config()
    • 转发底层事件到 D-Bus,当 shared 层(src/shared/bap.c)报告状态变化(如 stream started),它会通过回调反向通知 D-Bus。
  • 管理多角色
    • Profile 层同时支持:
      • BAP Client (Unicast Client):例如手机;
      • BAP Server (Unicast Server):例如耳机。

BAP Role

Unicast Audio

Unicast Audio是基于LE ACL Link,并且Client和Server之间在建立CIS连接前还需要进行Audio Capability交互和配置,例如Codec的参数匹配,ASE(Audio Stream Endpoint)发现和配置等,这部分主要涉及:

PACS = “我能干什么”

ASCS = “我现在要干什么”

PACS Service (UUID: 0x1850)

PACS service SPEC定义了如下特征值:

Characteristic 名称 UUID 功能说明
Sink PAC 0x2BC9 设备作为接收端(Sink)时支持的编解码能力
Source PAC 0x2BCA 设备作为发送端(Source)时支持的编解码能力
Sink Audio Locations 0x2BCE 音频输出位置(左耳、右耳、前置等)
Source Audio Locations 0x2BCF 音频输入位置(麦克风位置等)
Available Audio Contexts 0x2BD0 当前设备支持哪些音频场景(媒体、通话、紧急等)
Supported Audio Contexts 0x2BD1 所有可支持的音频场景(上限)

主要功能是让一个设备向远端公开它的 音频能力信息(Audio Capabilities),包括:

  • 支持哪些音频方向(输入/输出);
  • 支持哪些编解码器(Codec);
  • 每个编解码器支持的采样率、声道、比特率;
  • 支持的音频位置(左耳、右耳、前置、后置等);

HCI 实例
HCI PACS

从系统框图中可以看到,pipewire主要作用是Audio的处理和路由,所以也承担了对Audio的相关参数配置,以上参数的配置流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
sequenceDiagram
participant Pipewire
participant BlueZ

Pipewire->>Pipewire: Loading bluez factory<br/><impl_init()>
Pipewire->>BlueZ: Register Endpoint<br/>(D-Bus Method)

BlueZ->>BlueZ: Start bluetoothd daemon
BlueZ->>BlueZ: Update Endpoint Configuration
BlueZ->>BlueZ: Add PAC Service
BlueZ->>BlueZ: Start Adv

Pipewire在loading bluez插件后,会先读取bluez的配置文件(/etc/wireplumber/wireplumber.conf.d/bluetooth.conf), 并根据role加载对应的Endpoint, 如下表所示

BlueZ Role Endpoint Path
a2dp_sink /MediaEndpoint/A2DPSink/sbc
bap_sink /MediaEndpointLE/BAPSink/lc3
bap_source /MediaEndpointLE/BAPSource/lc3
bap_bcast_sink /MediaEndpointLE/BAPBroadcastSink/lc3
bap_bcast_source /MediaEndpointLE/BAPBroadcastSource/lc3

并且上面ATT交互中关于PACS中的sink pac特征值的参数如下表所配置,需要注意的是当前Pipewire还不支持PAC定义的全部内容,例如Metadata的解析,Locations和Audio Context的配置解析等。

1
2
3
4
5
6
7
8
9
10
11
12
...
bluez5.roles = [ a2dp_sink bap_sink bap_source bap_bcast_sink bap_bcast_source]
...

//bap pac audio capbility
bluez5.bap-server-capabilities.rates = [8000, 16000, 24000, 32000, 44100, 48000]
bluez5.bap-server-capabilities.durations = [10]
bluez5.bap-server-capabilities.channels = [1, 2]
bluez5.bap-server-capabilities.framelen_min = 20
bluez5.bap-server-capabilities.framelen_max = 120
bluez5.bap-server-capabilities.max_frames = 2
...

ASCS Service (UUID: 0x184E)

ASCS SPEC 定义了如下特征值:

Characteristic 名称 UUID 功能说明
ASE Control Point 0x2BC6 客户端用于发送控制命令(例如配置、启用、禁用、释放等)
Sink ASE 0x2BC4 表示服务端支持的一个音频接收端点(Audio Sink)
Source ASE 0x2BC5 表示服务端支持的一个音频发送端点(Audio Source)

主要功能:

  • 如何建立、配置、启用和停用 Audio Stream(即 ASE, Audio Stream Endpoint)
  • 如何在 Unicast Audio Server(通常是耳机)与 Unicast Audio Client(通常是手机或主机)之间协调音频流传输。

HCI 示例

HCI ASCS

下面是CIS source(Android phone) 与CIS sink (BA401 Board)建立和断开CIS link时ASCS service的交互图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
sequenceDiagram
%% 定义参与者
participant Phone as Android Phone
participant BA401 as BA401

%% 广播与扫描阶段
Phone->>Phone: scan
BA401->>BA401: register ASCS<br/>start ADV
BA401-->>Phone: Adv

%% 建链与发现
Phone->>BA401: LE Create connection
Phone->>BA401: Service Discovery

%% ASE 配置阶段
Phone->>BA401: Configure ASE
Phone->>BA401: Write ASE QoS/Enable
BA401-->>Phone: Notified QoS/Enabling
BA401->>BA401: Create transport
Phone->>BA401: Create CIS

%% 数据传输阶段
Phone->>BA401: ISO data
BA401->>BA401: Transport Active<br/>Pipewire fetch iso<br/>and push ALSA

%% 释放阶段
Phone->>BA401: Write ASE release
BA401-->>Phone: Notified releasing
Phone->>BA401: Remove CIS
BA401->>BA401: Transport idle

Broadcast Audio

Broadcast Source

Broadcast Source主要维护下面三层广播数据,并且Sink端通过对这些广播的解析和同步就可以收听到source端广播的音频数据。

BIS Source Data

层级 类型 数据类型 主要内容 描述
第一层 EA (Extended Advertising) ADV_EXT_IND
AUX_ADV_IND
AUX_CHAIN_IND
Broadcast ID、Announcement 数据 让设备被发现
第二层 PA (Periodic Advertising) AUX_SYNC_IND BASE、BIG Info PA 是 Auracast 广播的“控制层”,负责告诉接收方如何同步 BIS、如何解码音频。
第三层 BIS (Broadcast Isochronous Stream) BIS LC3 音频帧 真正的音频数据流传输

注释:

  • AdvA: 广播设备地址(Advertiser Address)
  • ADI: Advertising Data Info(包括 SID)
  • SyncInfo:如果设备开启了周期性广播(PA),这里会嵌入 PA 的同步信息;
  • ACAD:Advertising Common Additional Data,其中可以嵌入 BIGInfo(BIG 参数信息);
  • BASE:Broadcast Audio Stream Endpoint 信息,描述音频的格式、采样率、声道配置等;
  • AuxPtr:如果数据太多,会通过链式结构扩展(AUX_CHAIN_IND)。

Broadcast Audio Announcements

Broadcast source在Extend ADV中申明UUID:0x1852,另外Broadcast_ID 在BIG的声明周期中存在并具有唯一性,其左右是让Sink设备识别和确定指向感兴趣 BIG 的广播集。

下面是HCI示例:
HCI EA

Basic Audio Announcements

下图是BAP SPEC中定义的BIS source Codec配置和Metadata配置。
BIS Source BASE structure

当前Pipewire (V1.4.5)不支持subgroup的配置,所以只能设置1个BIG下多个BIS的情况,下面是示例按照AC14 (1路bises包含2通道数据)的格式设置的BIS source pac,其参数对应的PA ADV中的BASE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
bluez5.roles = [ a2dp_sink bap_sink bap_source bap_bcast_sink bap_bcast_source]
...

//bap broadcast source pac audio capbility
bluez5.bcast_source.config = [
{
"broadcast_code": "amlbis",
"encryption": false,
"sync_factor": 2,
"bis": [
{
"qos_preset": "48_2_2",
"audio_channel_allocation": 3,
"metadata": [
{ "type": 1, "value": [4,0]}
]
}
]
}
]
...

HCI 示例
HCI PA

Broadcast Source Stream States

1
2
3
4
5
6
7
stateDiagram-v2
[*] --> Idle
Idle --> Configured : BIS Configuration
Configured --> Streaming : BIS Establishment
Configured --> Idle : BIS Release
Streaming --> Configured : BIS Disable

Broadcast Sink With Assistant (BASS UUID: 0x184F)

BASS SPEC定了如下特征值。

Characteristic 名称 UUID 功能说明
Broadcast Receive State 0x2BC9 表示当前设备已知的广播源及其状态(最多多个实例)
Broadcast Audio Scan Control Point (BASS CP) 0x2BC7 控制操作入口,用于添加/删除/修改广播源或触发扫描

主要功能:

  • BASS Client = Assistant(通常是手机/控制器)
    • 手机作为 Assistant,负责扫描广播源(BIS Source)信息、决定哪个广播要接收、并通过 BASS Control Point 下发“加入某个广播源”的配置给 sink 设备(音箱/耳机的 sink 端)。
  • BASS Server = Broadcast Sink 设备(例如音箱 / earbud sink)
    -Sink 设备在本地实现 BASS 服务,接收 Assistant 的命令后去执行 PA sync / BIG sync,并把同步状态回报给 Assistant(通过 Broadcast Receive State 通知)。
  • BASS CP(Control Point)支持的opcode,Client->Server
    • Add Source:把一个 source 条目写入 sink(包含 adv SID、broadcast_id、bis list、scan params)
    • Remove Source:删除 source 条目
    • Modify Source:修改已添加的 source(例如更换 BIS 列表)
    • Remote Scan Start/Stop:指示 sink 开/关扫描(辅助寻找 source)
    • Set Broadcast Code:提供解密 key(若广播加密)
  • 状态反馈, Server->Client
    -Sink 用 Broadcast Receive State characteristic(read/notify)来上报每个 source 的状态(PA sync、BIG sync、NUM_BIS、BIS索引、是否加密、同步失败原因等)。Assistant 根据这些通知判断是否成功加入或需要重试/提示用户。

Assistant和BIS sink设备的交互流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sequenceDiagram
participant Assistant
participant BIS_Sink
participant BIS_Source

Note over Assistant, BIS_Source: Listen BIS Source 流程
Assistant->>BIS_Sink: Create LE Link
Assistant->>BIS_Sink: Control Point OP: Started
BIS_Sink->>BIS_Source: Enable Scan
Assistant->>BIS_Sink: Control Point OP: Add
BIS_Source->>BIS_Sink: PA Sync
BIS_Source->>BIS_Sink: BIG Sync
BIS_Sink-->>Assistant: Receive State Notify
Assistant->>BIS_Sink: Control Point OP: Stopped

Note over Assistant,BIS_Source: Remove BIS Source 流程
Assistant->>BIS_Sink: Control Point OP: Modify
BIS_Sink->>BIS_Source: PA Terminate
BIS_Sink->>BIS_Source: BIG Terminate
BIS_Sink-->>Assistant: Receive State Notify
Assistant->>BIS_Sink: Control Point OP: Remove
BIS_Sink-->>Assistant: Receive State Notify

BlueZ 中BASS 代码

BASS相关的代码主要实现在下面文件中:

src/shared/bass.c 该文件属于共享库层(shared library layer),主要定义了 BASS 协议的通用解析、编码和GATT操作逻辑(handlers),既可供 client 也可供 server 使用。

profiles/audio/bass.c 该文件属于 Profile层(蓝牙应用服务层),它构建在 src/shared/bass.c 之上,实现 BASS 作为一个完整的 BlueZ Profile,并通过 D-Bus 提供给上层系统(如PipeWire、PulseAudio或Android HAL)使用。

下面是BASS定义的操作码的处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct bass_op_handler {
const char *str;
uint8_t op;
size_t size;
void (*func)(struct bt_bass *bass,
struct gatt_db_attribute *attrib,
uint8_t opcode,
unsigned int id,
struct iovec *iov,
struct bt_att *att);
} bass_handlers[] = {
BASS_OP("Remote Scan Stopped", BT_BASS_REMOTE_SCAN_STOPPED,
0, bass_handle_remote_scan_stopped_op),
BASS_OP("Remote Scan Started", BT_BASS_REMOTE_SCAN_STARTED,
0, bass_handle_remote_scan_started_op),
BASS_OP("Remove Source", BT_BASS_REMOVE_SRC,
0, bass_handle_remove_src_op),
BASS_OP("Add Source", BT_BASS_ADD_SRC,
0, bass_handle_add_src_op),
BASS_OP("Set Broadcast Code", BT_BASS_SET_BCAST_CODE,
0, bass_handle_set_bcast_code_op),
BASS_OP("Modify Source", BT_BASS_MOD_SRC,
0, bass_handle_mod_src_op),
{}
};

Broadcast Sink With Scan Delegator

该组合是设备自己扫描附近的BIS source设备,然后根据一些策略来选择BIS source设备进行Sync,所以流程相对比较简单,比较重要的两个流程就是short PA和BIG sync,下面是流程图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sequenceDiagram
%% 定义参与者
participant App as App
participant BlueZ as BlueZ

BlueZ->>BlueZ: Start Bluetoothd deamon
BlueZ->>BlueZ: Enable LE Scan & Parse ADV Report
BlueZ->>BlueZ: Filter UUID:0x1852
BlueZ->>BlueZ: Short PA to get BIS source info
BlueZ->>App: Transport InterfacesAdd

App->>App: Record Transport(BIS source dev)
App->>App: Filter with RSSI & Vendor UUID

App->>BlueZ: Select & Acquire target Transport
BlueZ->>BlueZ: PA sync & BIG sync
BlueZ->>BlueZ: Streaming

Short PA的BlueZ代码

当扫描到包含UUID=0x1852的设备时会自动probe bap_bcast_profile 即调用bap_bcast_probe接口,然后在pa sync成功的回调函数big_info_report_cb中关闭对应的socket,在kernel中就会发起terminal PA的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// profiles/audio/bap.c
bap_bcast_probe() {
...
pa_sync(data);
...
}

big_info_report_cb() {
...
/* Close the listen io */
g_io_channel_shutdown(data->listen_io, TRUE, NULL);
g_io_channel_unref(data->listen_io);
data->listen_io = NULL;

/* For short-lived PA, the sync is no longer needed at
* this point, so the io can be closed.
*/
g_io_channel_shutdown(io, TRUE, NULL);
...
}

Formal PA Sync and BIG Sync,当bt_mgr 选择了目标BIS source设备,即调用transport.Select以及transport.Acquire方法,接下来transport状态会被设置会Enabling,调用**bap_state_bcast_sink() ->setup_create_bcast_io() **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// profiles/audio/transport.c
static const GDBusMethodTable transport_methods[] = {
{ GDBUS_ASYNC_METHOD("Acquire",
NULL,
GDBUS_ARGS({ "fd", "h" }, { "mtu_r", "q" },
{ "mtu_w", "q" }),
acquire) },
{ GDBUS_ASYNC_METHOD("TryAcquire",
NULL,
GDBUS_ARGS({ "fd", "h" }, { "mtu_r", "q" },
{ "mtu_w", "q" }),
try_acquire) },
{ GDBUS_ASYNC_METHOD("Release", NULL, NULL, release) },
{ GDBUS_ASYNC_METHOD("Select",
NULL, NULL, select_transport) },
{ GDBUS_ASYNC_METHOD("Unselect",
NULL, NULL, unselect_transport) },
{ },
};

// profiles/audio/bap.c
static void setup_create_bcast_io(struct bap_data *data,
struct bap_setup *setup,
struct bt_bap_stream *stream, int defer)
{
...
iso_qos.bcast.big = setup->qos.bcast.big;
iso_qos.bcast.bis = setup->qos.bcast.bis;
iso_qos.bcast.sync_factor = setup->qos.bcast.sync_factor;
iso_qos.bcast.packing = setup->qos.bcast.packing;
iso_qos.bcast.framing = setup->qos.bcast.framing;
iso_qos.bcast.encryption = setup->qos.bcast.encryption;
if (bcode && bcode->iov_base)
memcpy(iso_qos.bcast.bcode, bcode->iov_base, bcode->iov_len);
iso_qos.bcast.options = setup->qos.bcast.options;
iso_qos.bcast.skip = setup->qos.bcast.skip;
iso_qos.bcast.sync_timeout = setup->qos.bcast.sync_timeout;
iso_qos.bcast.sync_cte_type = setup->qos.bcast.sync_cte_type;
iso_qos.bcast.mse = setup->qos.bcast.mse;
iso_qos.bcast.timeout = setup->qos.bcast.timeout;
memcpy(&iso_qos.bcast.out, &setup->qos.bcast.io_qos,
sizeof(struct bt_iso_io_qos));

...
setup_accept_io_broadcast(data, setup);
}

下面是bluetoothd.log中PA sync和BIG sync部分的内容,可以很直观的看到大概的调用流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bluetoothd[2490]: [:1.3:method_call] > org.bluez.MediaTransport1.Acquire [#35]
bluetoothd[2490]: profiles/audio/transport.c:media_owner_create() Owner created: sender=:1.3
bluetoothd[2490]: profiles/audio/transport.c:media_request_create() Request created: method=Acquire id=0
bluetoothd[2490]: profiles/audio/transport.c:media_owner_add() Owner :1.3 Request Acquire
bluetoothd[2490]: profiles/audio/transport.c:media_transport_set_owner() Transport /org/bluez/hci0/dev_00_59_BA_D6_B8_B3/sid0/bis1/fd1 Owner :1.3
bluetoothd[2490]: profiles/audio/transport.c:media_transport_resume() Transport /org/bluez/hci0/dev_00_59_BA_D6_B8_B3/sid0/bis1/fd1 Owner :1.3
bluetoothd[2490]: profiles/audio/transport.c:transport_bap_update_links_bc() stream 0x8d7958 linked false
bluetoothd[2490]: src/shared/bap.c:bap_bcast_set_state() stream 0x8d7958 dir 0x04: config -> enabling
bluetoothd[2490]: profiles/audio/bap.c:bap_state_bcast_sink() stream 0x8d7958: config(1) -> enabling(3)
bluetoothd[2490]: profiles/audio/bap.c:setup_create_io() setup 0x8d5550 stream 0x8d7958 defer false
bluetoothd[2490]: profiles/audio/bap.c:pa_and_big_sync() Create PA sync with this source
bluetoothd[2490]: profiles/audio/transport.c:bap_state_changed() stream 0x8d7958: config(1) -> enabling(3)
bluetoothd[2490]: [signal] org.freedesktop.DBus.Properties.PropertiesChanged
bluetoothd[2490]: profiles/audio/bap.c:iso_do_big_sync() PA Sync done
bluetoothd[2490]: profiles/audio/bap.c:append_setup() Do BIG Sync with BIS 1