docs(ble_audio): Add documents for introducing ISO & LE Audio architectures

This commit is contained in:
Liu Linyan
2026-06-10 19:44:33 +08:00
parent a96553ed75
commit 8ffb543127
18 changed files with 3156 additions and 240 deletions
@@ -0,0 +1,585 @@
.. _arch-iso-transport:
ESP-BLE-ISO
===========
:link_to_translation:`zh_CN:[中文]`
.. |br| raw:: html
<br/>
.. contents:: Table of Contents
:local:
:depth: 2
ESP-BLE-ISO provides every BLE transport primitive an upper-layer profile needs: ACL connection state, advertising and scanning, periodic-advertising sync, the HCI command path, ISO (CIS and BIS), GATT, GAP and L2CAP, and the ISO task event loop and global lock described in :ref:`Concurrency and Thread Safety <arch-concurrency>`. None of it is audio-specific; ESP-IDF Bluetooth LE Audio is simply the profile layer that currently sits on top. This chapter covers the whole component in turn: connection management, advertising and scanning, the HCI command path, the ISO subsystem, GAP, GATT and L2CAP, and the public API.
Two conventions recur throughout the component and are worth stating once:
- **The** ``_safe`` **suffix.** Most internal operations exist as a pair: a core function that assumes the global ISO lock is already held, and a wrapper of the same name with a ``_safe`` suffix that acquires the lock, calls the core function, and releases it. Public entry points and callers from outside the ISO task use the ``_safe`` variant; code already running under the lock (an ISO task handler, or another core function) calls the bare variant directly. This is how the single global lock from :ref:`Protecting Shared State <arch-host-lock>` is applied uniformly without re-entering it accidentally -- although the mutex is recursive, the split keeps the lock boundary explicit at every call site.
- **The** ``bt_le_*`` / ``bt_*`` **naming.** Symbols beginning with ``bt_`` are the host-agnostic interface defined in ``host/common``; symbols beginning with ``bt_le_bluedroid_`` or ``bt_le_nimble_`` are the adapter implementations that exactly one build compiles.
- One family is exempt: the ``hci_le_*`` ISO event handlers in ``host/common`` (for example ``hci_le_biginfo_adv_report``) are named after the HCI event they decode rather than the ``bt_`` interface, and are internal handlers rather than part of the public surface.
.. _arch-concurrency:
Concurrency and Thread Safety
-----------------------------
All ESP-IDF Bluetooth LE Audio processing above the host stack runs on a single task -- the **ISO task** (``iso_task``) -- and all access to shared state is serialized by a single global recursive mutex, the **ISO lock** (``bt_le_host_lock``). These two mechanisms together are what make the stack thread-safe; understanding them is the key to reasoning about ordering and races.
ISO Task Event Loop
~~~~~~~~~~~~~~~~~~~
The ISO task (in ``host/common/task.c``) is a single FreeRTOS task that loops forever, draining work the rest of the stack posts to it with ``bt_le_iso_task_post()``. Each item carries an event-type tag, and the task dispatches it to the handler for that category -- timer, GAP, GATT, ISO HCI, ISO transmit-complete, or ISO receive-data.
Posted work is not one queue but **three priority tiers**, each a separate FreeRTOS queue, joined into one queue set the task blocks on. On each wakeup the task services the tiers **strictly by priority** -- critical first, then normal, then floodable -- processing one item per loop and re-checking the critical tier first on the next:
.. list-table:: ISO task priority tiers
:header-rows: 1
:widths: 16 12 22 50
* - Tier
- Depth
- On overflow
- Events
* - **Critical**
- 32
- drop newest
- ISO receive-data and transmit-complete -- the latency-critical data path, posted non-blocking from the controller task.
* - **Normal**
- 64
- blocks (never dropped)
- Timer, GAP lifecycle, GATT and ISO HCI events -- reliable; the producer blocks until space frees.
* - **Floodable**
- 32
- drop newest
- High-volume best-effort GAP reports -- extended-advertising, periodic-advertising and BIGInfo reports -- posted non-blocking.
The split exists so a burst of GAP reports cannot delay ISO data: the critical tier is always drained first, and the two non-blocking tiers drop their newest item rather than stall their producer when full. Which tier an event lands in is independent of the handler that runs it -- the advertising and periodic-advertising reports on the floodable tier are dispatched by the same ``bt_le_gap_handle_event`` as the normal-tier GAP events, and a floodable BIGInfo report by the same ``bt_le_iso_handle_hci_event`` as a normal ISO HCI event.
.. note::
A single task drains all three tiers, so events are still processed **one at a time and never overlap**, whatever task produced them -- the serialization guarantee the rest of the stack relies on is unchanged. What the tiers change is **ordering**: events are dispatched in priority order, not in the order they were posted, so a higher-priority ISO data event can be serviced ahead of an earlier-posted GAP report. Code that relies on the serialization guarantee stays correct; code that assumed strict first-in, first-out ordering across event categories does not.
A few properties of the task are worth keeping in mind:
- It runs on a **4 KB stack**. Every upper layer, including application and profile callbacks, executes on this stack, so deep call chains and large stack-allocated buffers (for example arrays sized from configuration values) must be avoided.
- Each queued item carries an event-type tag and a heap-allocated payload; the producer allocates the payload and the handler frees it after processing -- except an item dropped on a full non-blocking tier, which the producer frees instead.
- Its CPU core and priority are matched to the active host stack, as described in :ref:`Dual-Host Design <arch-dual-host>`.
An optional **dispatch monitor** (``CONFIG_BT_ISO_DISPATCH_MONITOR``, off by default) times every callback the task dispatches and keeps per-event-type statistics -- a count, the maximum duration, and a slow-count for callbacks exceeding ``CONFIG_BT_ISO_DISPATCH_THRESHOLD_US`` (default 2000 us, about a quarter of a 7.5--10 ms SDU interval). The table is logged periodically (``CONFIG_BT_ISO_DISPATCH_DUMP_PERIOD_S``, default 10 s) and at deinit, to surface a callback that runs long enough to delay the ISO data path. It adds per-dispatch timing overhead and measures wall-clock time, so it is intended for profiling only.
.. _arch-host-lock:
Protecting Shared State
~~~~~~~~~~~~~~~~~~~~~~~
Shared state -- the connection table, the periodic-advertising sync table, the GATT subscription lists, the GATT-server configuration, the ISO bookkeeping -- is reachable from more than one task. The stack does **not** use fine-grained, per-structure locks. Instead, thread safety relies on one coarse-grained **global recursive mutex** -- the **ISO lock**, ``bt_le_host_lock`` / ``bt_le_host_unlock`` (implemented in ``host/common/host.c`` over a Zephyr ``k_mutex``).
The rule is simple and applies everywhere: **any code path that touches shared state acquires this one mutex first.** In practice that means:
- **Public API functions**, called from the application task, take the lock on entry and release it before returning.
- **ISO task event handlers** take the lock around the part of each handler that reads or mutates shared state.
- **NimBLE synchronous callbacks** (subscription changes, received notifications, the GATT-server attribute access path) take the lock before touching shared state.
- **Timer callbacks** take the lock as well.
Because every reader and writer of a given variable holds the same lock, no two tasks can ever touch it concurrently. The mutex is **recursive**, so a handler that already holds the lock can call helper functions that re-acquire it without deadlocking.
If the lock cannot be acquired within a short bounded time, ``bt_le_host_lock`` calls ``abort()`` rather than continuing. A lock that is held that long means the stack is wedged (a deadlock or a stuck callback), which is a programming error rather than a runtime condition to recover from. ``abort()`` is used deliberately instead of ``assert()``, because ``assert()`` becomes a no-op in ``NDEBUG`` builds and would let the caller enter the critical section without the lock held, reintroducing the race. The unlock path additionally verifies that the calling task is the current holder, catching unbalanced unlocks.
.. important::
Two distinct serialization mechanisms are in play, and they cover different paths:
#. The **single consumer** behind the three priority queues serializes everything *posted* to the ISO task (the GAP, GATT, ISO and timer events that originate from the host stack). Posted events are processed one at a time and never overlap, though across tiers they are dispatched in priority order rather than post order.
#. The **global ISO lock** serializes the *other* contexts -- direct application API calls and the NimBLE synchronous callbacks -- against the ISO task handlers.
A bug report that assumes two posted events run concurrently is almost always a false positive: the single consumer rules that out.
.. _arch-callback-context:
Callback Execution Context
~~~~~~~~~~~~~~~~~~~~~~~~~~
A callback that the application registers (a GAP event handler, a GATT attribute handler, a profile event callback) does not always run on the same task, and the task **differs between the two hosts** for one important case. Knowing the context matters: a callback that runs on the ISO task shares its 4 KB stack and must not block it, while a callback that runs on the host task executes synchronously inside the host's attribute-access path.
.. list-table:: Which task a registered callback runs on
:header-rows: 1
:widths: 44 28 28
* - Callback category
- Bluedroid
- NimBLE
* - GAP events (connect, disconnect, security change, PA sync)
- ISO task
- ISO task
* - GATT client notifications and discovery results
- ISO task
- ISO task
* - GATT client read and write completions
- ISO task
- **NimBLE host task**
* - ISO events (connect, data, transmit-complete)
- ISO task
- ISO task
* - Timer callbacks
- ISO task
- ISO task
* - GATT-server attribute read/write and CCC subscription, and any profile callback driven synchronously by an inbound write
- ISO task
- **NimBLE host task**
Two rows place a callback on the NimBLE host task rather than the ISO task. The GATT-client read and write completions land there because NimBLE reports a procedure result through a completion callback on its host task, and the adapter invokes the application callback inline under the ISO lock instead of re-posting it -- whereas Bluedroid posts every BTA GATT event to the ISO task. The GATT-server attribute-access row has a sharper cause: the two hosts complete the access in opposite ways. On Bluedroid the request is posted to the ISO task, the registered callback runs there, and the response is sent **asynchronously** afterwards, as illustrated below:
.. mermaid::
sequenceDiagram
participant P as Peer
participant BTU as BTU task
participant T as ISO task
participant CB as attr read/write callback
P->>BTU: ATT read / write request
BTU->>T: post GATTS event
T->>CB: invoke callback (under ISO lock)
CB-->>T: value / status
T->>BTU: BTA_GATTS_SendRsp (async)
BTU-->>P: ATT response
On NimBLE, by contrast, the host requires the value to be returned **synchronously**, so the callback runs inline on the host task and hands the value straight back:
.. mermaid::
sequenceDiagram
participant P as Peer
participant N as NimBLE host task
participant CB as attr read/write callback
P->>N: ATT read / write request
N->>CB: invoke callback inline (under ISO lock)
CB-->>N: value / status
N-->>P: send response synchronously
.. _arch-event-flow:
Cross-Layer Event Flow
~~~~~~~~~~~~~~~~~~~~~~
The diagram below traces a typical inbound event -- a GAP, GATT-client or ISO event raised by the controller -- from the controller up to the application callback, and shows where the two hosts differ.
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant C as BLE controller
participant H as Host stack task
participant A as ESP-BLE-ISO adapter
participant T as ISO task
participant CB as App/Profile callbacks
C->>H: HCI event
Note over H: Bluedroid: received on the HCI host task, callback runs on BTU<br/>NimBLE: one host task does both
H->>A: host callback
Note over A: Bluedroid: BTA callback<br/>NimBLE: *_cb_safe (takes ISO lock)
A->>A: build event payload on the heap
A->>T: bt_le_iso_task_post() — enqueue
T->>T: dequeue, take ISO lock
T->>CB: invoke callback in ISO task context
Note over C,T: ISO data / tx-complete is the exception — the controller calls a registered callback directly, bypassing the Host stack
C-->>T: bt_le_iso_task_post() — enqueue (direct callback, on the controller task)
T->>T: dequeue, take ISO lock
T->>CB: channel callback in ISO task context
Where the application and profile callbacks run follows from this flow, and it differs by host:
- On **Bluedroid**, **every upper-layer callback runs in the ISO task context**, whatever triggered it -- GAP, GATT client (notification, discovery, read or write completion), GATT server, ISO or timer. The adapter posts every BTA event to the queue, so there is no exception. These callbacks run on the task's 4 KB stack while the ISO lock is held, and calling a blocking transport API from one stalls the entire event loop.
- On **NimBLE**, the same is true *except* for two categories, which run **inline on the NimBLE host task** rather than the ISO task: **GATT-server attribute access** (the value must be returned synchronously) and **GATT-client read and write completions** (NimBLE reports the result through a host-task completion callback that the adapter invokes directly). A profile callback driven by either -- for example a server-side control-point write handler, or a client read or write completion -- therefore runs on the host task, not the ISO task, and does not pass through the queue. These are the two rows flagged in :ref:`Callback Execution Context <arch-callback-context>`.
Layer and File Map
------------------
The component is organized into a host-agnostic ``host/common`` layer, the two ``host/adapter`` layers, a standalone ISO engine under ``host/iso``, and shared helpers under ``host/utils``. The host-agnostic files are:
.. list-table:: ``host/common`` -- host-agnostic transport
:header-rows: 1
:widths: 24 76
* - File
- Role
* - ``host.c``
- Init and deinit orchestration; the global recursive lock (``bt_le_host_lock`` / ``bt_le_host_unlock``).
* - ``task.c``
- The ISO task event loop, its three priority queues and queue set, and ``bt_le_iso_task_post()``.
* - ``conn.c``
- ACL connection table and the connection-event listener fan-out.
* - ``adv.c``
- Extended-advertising set bookkeeping.
* - ``scan.c``
- Scanning, periodic-advertising sync, and BIGInfo reports.
* - ``hci.c``
- Builds HCI command buffers and dispatches them to the active adapter.
* - ``iso.c``
- Decodes ISO HCI meta-events and bridges the ISO data path to the engine.
* - ``gatt.c``
- Host-agnostic GATT state (subscriptions, attribute database cache).
* - ``l2cap.c``
- Generic L2CAP channel and server dispatch.
* - ``app/gap.c``
- The application-facing GAP entry points.
* - ``app/gatt.c``
- The application-facing GATT entry points.
The ISO state machine lives apart from the rest of the common layer, in ``host/iso/iso.c``: it owns the CIG, BIG and ISO-channel logic and the static, configuration-sized pools that back them. It is the single file under ``host/iso`` and is detailed in :ref:`ISO Subsystem <arch-iso>` below.
Each adapter directory mirrors the common interface for its host: ``host/adapter/bluedroid`` and ``host/adapter/nimble`` each provide ``gap.c``, ``gatt``, ``iso.c`` and the host-specific headers, and ``host/utils`` holds address, UUID, CRC, crypto, timer and buffer helpers that neither layer needs to reimplement.
.. note::
Two files named ``iso.c`` exist and play different roles. ``host/iso/iso.c`` is the engine -- the CIS and BIG state machines and the public ISO channel API. ``host/common/iso.c`` is the glue -- it decodes raw HCI ISO meta-events into the engine's handlers and carries the ISO data tx/rx path. The adapters' ``iso.c`` files translate engine commands into host-native calls.
Connection Management
---------------------
``conn.c`` owns the ACL connection table -- an array of ``bt_conn`` objects keyed by connection handle -- and the registration list for connection-event listeners. The connection lifecycle is driven entirely from the host adapter: when the controller reports a connection event, the adapter calls one of the listener entry points, which update the table and fan the event out to every ``bt_conn_cb`` the upper layers registered with ``bt_conn_cb_register``.
The listener entry points form the host-agnostic seam:
- ``bt_le_acl_conn_connected_listener`` and ``bt_le_acl_conn_disconnected_listener`` add and remove table entries.
- ``bt_le_acl_conn_security_changed_listener``, ``..._identity_resolved_listener``, ``..._pairing_completed_listener`` and ``..._bond_deleted_listener`` carry the security and bonding events.
Because the adapter posts these through the ISO task (see :ref:`Dual-Host Design <arch-dual-host>`), the registered ``bt_conn_cb`` callbacks run in the ISO task context on both hosts -- the GAP-events row of :ref:`Callback Execution Context <arch-callback-context>`. Operations that act on a live connection (``bt_conn_set_security``, ``bt_conn_disconnect``, ``bt_conn_get_info``) and the lookup helpers (``bt_conn_lookup_handle``, ``bt_le_acl_conn_find``, ``bt_conn_foreach``) read from the same table under the ISO lock. The host-specific *sourcing* of these events -- which host task the adapter is on when it calls a listener, and how it maps native event structures -- is covered under :ref:`Application Event Interface <arch-app-event>` below.
Advertising and Scanning
------------------------
``adv.c`` is deliberately small: it tracks extended-advertising *sets* by handle (``bt_le_ext_adv_find`` / ``bt_le_ext_adv_new_safe`` / ``bt_le_ext_adv_delete_safe``). An advertising set is the anchor a broadcast ISO group attaches to, so this table is a prerequisite for the BIG broadcaster flow in the ISO subsystem.
``scan.c`` covers three related responsibilities:
- **Scanning.** The ``bt_le_scan_cb`` registry; advertising reports arrive at ``bt_le_scan_recv_listener`` and are delivered to the application.
- **Periodic-advertising sync.** The sync table (``bt_le_per_adv_sync_new`` / ``..._delete`` / ``..._lookup_addr``) and its listeners -- ``..._establish_listener``, ``..._lost_listener`` and ``..._report_recv_listener`` -- track synchronization to a broadcaster's periodic train.
- **BIGInfo reports.** ``hci_le_biginfo_adv_report`` surfaces the BIGInfo that rides a periodic-advertising train. BIGInfo carries the parameters a receiver needs to synchronize to a Broadcast Isochronous Group, so this handler is the bridge from scanning into the BIG-receiver flow.
Periodic-advertising sync is therefore the entry point for *receiving* broadcast audio: scan, synchronize to the periodic train, read the BIGInfo, and then ask the ISO subsystem to synchronize to the BIG.
HCI Command Path
----------------
The ISO engine never talks to a host stack directly. It builds a standard HCI command buffer with ``bt_hci_cmd_create(opcode, len)`` and submits it with ``bt_hci_cmd_send_sync(opcode, buf, rsp)`` (both in ``host/common/hci.c``). ``bt_hci_cmd_send_sync`` is a one-line dispatcher to the active adapter's ISO command entry point, and this is where the two hosts diverge sharply:
.. list-table:: HCI command translation per host
:header-rows: 1
:widths: 20 40 40
* - Aspect
- Bluedroid
- NimBLE
* - Entry point
- ``bt_le_bluedroid_iso_cmd_send_sync``
- ``bt_le_nimble_iso_cmd_send_sync``
* - Translation
- An opcode switch; each command is forwarded as a raw HCI command over a private *direct-HCI* path.
- An opcode switch; each command is unpacked into NimBLE's typed ``ble_hs_hci_*`` ISO helpers.
* - Synchronization
- A private completion semaphore in ``adapter/bluedroid/hci.c``; the completion callback runs on the HCI-layer task.
- Handled inside the ``ble_hs_hci_*`` call.
The reason Bluedroid carries its own command path is concurrency. Bluedroid's BTU task uses a single global slot to match a synchronous command to its completion; issuing ISO commands from the ISO task through that same slot would race the BTU task. The direct-HCI path in ``adapter/bluedroid/hci.c`` sidesteps this entirely: each command carries its own completion callback and the caller waits on a dedicated semaphore, so the shared BTU slot is never touched. The callback runs on the HCI-layer task and is kept minimal -- it copies the response into a static sink and signals the semaphore, taking no lock. NimBLE needs none of this, because its ``ble_hs_hci_*`` helpers already provide a self-contained synchronous command interface.
.. mermaid::
sequenceDiagram
participant CORE as ISO engine (host/iso/iso.c)
participant HCI as bt_hci_cmd_send_sync (common/hci.c)
participant AD as Active adapter
participant CTRL as Controller
CORE->>HCI: build command buffer (opcode + params)
HCI->>AD: *_iso_cmd_send_sync(opcode)
Note over AD: Bluedroid: direct-HCI (private sem)<br/>NimBLE: ble_hs_hci_* typed helper
AD->>CTRL: HCI command
alt Command Complete (e.g. Set CIG Parameters)
CTRL-->>AD: Command Complete
AD-->>CORE: status + return parameters
else Command Status (e.g. Create CIS)
CTRL-->>AD: Command Status
AD-->>CORE: status (no return parameters)
Note over CORE,CTRL: real outcome arrives later as an LE meta-event<br/>(e.g. CIS Established),<br/>handled in the ISO Subsystem below
end
.. _arch-iso:
ISO Subsystem
-------------
The ISO subsystem is split across three places: the engine (``host/iso/iso.c``), the meta-event and data glue (``host/common/iso.c``), and the adapters (``host/adapter/*/iso.c``). The engine owns three static, configuration-sized pools -- one for ISO channels (``CONFIG_BT_ISO_MAX_CHAN``), one for Connected Isochronous Groups (``CONFIG_BT_ISO_MAX_CIG``), and one for Broadcast Isochronous Groups (``CONFIG_BT_ISO_MAX_BIG``). Every public engine operation follows the ``_safe`` convention from the start of this section.
Inbound ISO meta-events follow the same shape on both hosts: the adapter registers a host-native ISO event callback, packages the event, and posts an ``ISO_HCI_EVENT`` item to the ISO task; ``host/common/iso.c`` then decodes the LE subevent and dispatches it to the engine. One wire-format difference is absorbed here: NimBLE's meta-event structures already include the subevent code, while the Bluedroid adapter prepends it, so the decoder in ``host/common/iso.c`` sees a uniform layout regardless of host.
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant CTRL as Controller
participant AD as Active adapter
participant T as ISO task
participant CORE as ISO engine
participant APP as App callback
CTRL->>AD: ISO meta-event (HCI)
Note over AD: Bluedroid: BTM ISO callback (BTU task)<br/>NimBLE: ISO callback (host task)
AD->>T: post ISO_HCI_EVENT
CTRL-->>T: ISO data, post ISO_RX_DATA
T->>CORE: handle in common/iso.c, then engine handler
CORE->>APP: channel callback (under ISO lock)
Connected ISO (CIS)
~~~~~~~~~~~~~~~~~~~
A Connected Isochronous Stream is point-to-point and rides an ACL connection. The two roles drive the engine through different entry points:
.. list-table:: CIS roles
:header-rows: 1
:widths: 16 44 40
* - Role
- Flow
- Key symbols
* - Central
- Configure a group, then establish one or more streams to the peers; the controller confirms each stream as it comes up.
- ``bt_iso_cig_create``,\ |br|\ ``bt_iso_cig_reconfigure``,\ |br|\ ``bt_iso_cig_terminate``,\ |br|\ ``bt_iso_chan_connect`` / ``hci_le_cis_established``
* - Peripheral
- Register a server that decides whether to accept incoming streams; each peer request is then accepted or rejected and, if accepted, confirmed.
- ``bt_iso_server_register`` /\ |br|\ ``hci_le_cis_req`` / ``hci_le_cis_established``
Broadcast ISO (BIG)
~~~~~~~~~~~~~~~~~~~
A Broadcast Isochronous Group is connectionless and rides a periodic-advertising train rather than an ACL connection:
.. list-table:: BIG roles
:header-rows: 1
:widths: 16 44 40
* - Role
- Flow
- Key symbols
* - Broadcaster
- Attach a BIG to an extended-advertising set that carries a periodic train; the controller confirms the group, after which the streams can be fed.
- ``bt_iso_big_create`` / ``hci_le_big_complete``,\ |br|\ ``bt_iso_big_terminate`` / ``hci_le_big_terminate``
* - Receiver
- Synchronize to the periodic train, read its BIGInfo, then synchronize to the group; loss of sync is reported back.
- ``bt_iso_big_sync`` / ``hci_le_big_sync_established``,\ |br|\ ``hci_le_big_sync_lost``
The receiver flow begins in ``scan.c``: the BIGInfo delivered by ``hci_le_biginfo_adv_report`` on an established periodic-advertising sync is what ``bt_iso_big_sync`` consumes to join the group.
Data Path and Data Flow
~~~~~~~~~~~~~~~~~~~~~~~
Establishing a CIS or a BIS is not enough to move audio; each stream must be bound to the controller's ISO data path. ``bt_iso_setup_data_path`` binds a direction (input for transmit, output for receive) and ``bt_iso_remove_data_path`` releases it. A stream that is connected but has no data path set up carries no SDUs: establishing the stream and binding its data path are separate steps, so audio does not flow until an upper layer explicitly sets up the path.
Once a data path is set up, SDUs flow through ``host/common/iso.c``:
- **Transmit.** The application submits an SDU with ``bt_iso_chan_send`` (or the timestamped ``bt_iso_chan_send_ts``); the engine hands it to ``bt_le_iso_tx``. If the controller has a free buffer and nothing is queued ahead, the SDU is sent straight to the controller; otherwise it is held in a host-side TX queue. The controller's transmit-complete signal returns as an ``ISO_TX_COMP`` event posted to the ISO task, where ``bt_le_iso_handle_tx_comp`` sends as many queued SDUs as the controller now has free buffers for, then invokes the application's transmit-complete callback.
- **Receive.** An incoming SDU is delivered by the adapter, packaged, and posted as an ``ISO_RX_DATA`` event; ``bt_le_iso_handle_rx_data`` passes it to ``bt_iso_recv`` in the engine, which invokes the channel's receive callback.
Both directions converge on the ISO task: transmit completion and receive delivery are ordinary queued events, so the application's ISO callbacks run in the ISO task context on its 4 KB stack, under the ISO lock, exactly like every other queued event in :ref:`Cross-Layer Event Flow <arch-event-flow>`.
The remaining ESP-BLE-ISO transport primitives are GATT, GAP and L2CAP. As with the rest of the component, the host-agnostic API (``bt_gatt_*``, ``bt_l2cap_*`` and the GAP helpers) lives in ``host/common`` and dispatches to whichever adapter is compiled.
GATT
----
GATT spans a **client** side (this device discovering, reading, writing and subscribing on a peer), a **server** side (this device exposing attributes a peer accesses), and the ATT **MTU exchange** that bounds both. All three are implemented in ``host/common/gatt.c`` over the active adapter.
Client
~~~~~~
The host-agnostic client API in ``host/common/gatt.c`` -- ``bt_gatt_discover``, ``bt_gatt_read``, ``bt_gatt_write``, ``bt_gatt_write_without_response_cb``, ``bt_gatt_subscribe`` and ``bt_gatt_unsubscribe`` -- dispatches to the active adapter's ``bt_le_*_gattc_*`` implementation. The two adapters back the same API with very different machinery:
.. list-table:: GATT client backing per host
:header-rows: 1
:widths: 22 39 39
* - Aspect
- Bluedroid
- NimBLE
* - Discovery
- BTA GATTC procedures; BTA owns the discovered attribute cache.
- A one-time full attribute-table walk cached in ``gatt.db.c``.
* - Procedure serialization
- BTA GATTC's own per-connection queue.
- The NRP queue in ``gatt.nrp.c``; params and data are deep-copied on insert.
* - Result delivery
- Every BTA GATT event is posted to the ISO task.
- Notifications and ``bt_gatt_discover`` results are posted to the ISO task; the peer attribute-table walk and the read and write completions run inline on the host task.
Discovery
^^^^^^^^^
Client discovery has two distinct flows that run on different tasks. The split exists because the hosts differ in whether they keep an attribute cache: Bluedroid's BTA GATTC discovers a peer's attribute table and caches it automatically, whereas NimBLE does not -- so the port adds that cache in ``gatt.db.c``.
**Walking the peer's attribute table (building the cache).** When the upper layer calls ``bt_gattc_disc_start``, the NimBLE adapter (``bt_le_nimble_gattc_db_auto_disc``) runs real ATT discovery against the peer with NimBLE's ``ble_gattc_disc_*`` procedures, walking the whole table once -- every primary service, included service, characteristic and descriptor, including each CCCD. These procedure callbacks run on the **NimBLE host task** and populate the per-connection cached database; the upper layer is notified on the ISO task only once the walk completes. On Bluedroid this step is implicit: ``bt_le_bluedroid_gattc_disc_start`` drives BTA GATTC, which performs the ATT procedures and maintains its own cache.
**Serving a discovery request.** When the upper layer -- for example the audio profiles -- calls ``bt_gatt_discover`` for specific attributes, the NimBLE adapter posts a discovery event to the **ISO task**, where the cached hierarchy answers the request locally with no further ATT traffic and the caller's callback is invoked. On Bluedroid the equivalent results surface from BTA's cache as discovery events on the ISO task.
Subscription Lifecycle
^^^^^^^^^^^^^^^^^^^^^^
Subscriptions are tracked host-agnostically. ``bt_gatt_subscribe`` records each ``bt_gatt_subscribe_params`` on a per-connection list (``gattc_sub``) and, unless an equivalent subscription already exists, writes the CCC through the adapter. Two behaviors are worth noting:
- The subscription is appended to the list *before* the CCC write completes, because some peers send the first notification before replying to the CCC write.
- The ``subscribe`` callback is invoked **synchronously**, completing the procedure in the caller's context rather than after the CCC-write response. On NimBLE this pairs with the cached database: the CCCD handle is already known, so no discovery round-trip is needed and the subscribe path does not block.
On disconnect, ``bt_le_acl_conn_disconnected_gatt_listener`` cleans up in a fixed order: the upper-layer disconnect callbacks run first, then ``gattc_sub_clear`` walks the subscription list, clears each entry's handles and value, and re-initializes the list. Upper layers must therefore **not** zero their own subscribe params inside a disconnect callback -- the params are still linked on the list at that point, and the common layer clears them immediately afterwards.
Procedure Serialization
^^^^^^^^^^^^^^^^^^^^^^^
**Background.** ``gatt.nrp.c`` exists for the same reason as ``gatt.db.c``: it supplies a service BTA GATTC provides natively but NimBLE does not. ATT permits only one outstanding request per connection at a time; BTA GATTC hides that behind its own per-connection procedure queue, whereas NimBLE leaves the caller to honor it.
**What NRP does.** The NRP (need-response PDU) queue in ``gatt.nrp.c`` is the NimBLE-side equivalent: a single per-connection queue spanning every response-bearing PDU -- the client's reads, writes and subscriptions, plus the server's indications (described in the server section below) -- so only one is in flight at a time and the rest wait their turn.
**Enqueue rules.** For the client procedures it bridges to the params-with-callback model the upper layers expect: ``bt_le_nimble_gatt_nrp_insert`` enqueues a read, write or subscribe -- dispatching it at once when the queue is idle, otherwise holding it until the in-flight procedure completes. ``bt_le_nimble_gatt_nrp_remove`` completes the head entry and starts the next; ``bt_le_nimble_gatt_nrp_clear`` discards pending entries on disconnect. Reads come in three forms -- by-UUID, long (fragmented) and single -- each delivering one event per result with explicit end-of-procedure handling.
**Deep copy.** ``bt_le_nimble_gatt_nrp_insert`` deep-copies the params and any data so the caller may pass a stack buffer and free it on return.
**EATT note.** The one-in-flight rule follows ATT's model of a single bearer with one outstanding request per connection. EATT introduces multiple concurrent bearers per connection, so supporting it would mean revisiting the NRP queue to allow per-bearer concurrency rather than a single procedure in flight.
The callback context follows from this. NimBLE delivers a read or write completion on its host task, and the NRP handler invokes the application callback there, under the ISO lock -- the same host-task context as the GATT-server attribute access. Bluedroid instead posts every BTA GATT completion to the ISO task, so the corresponding callbacks run there. This is the read-and-write-completion row of :ref:`Callback Execution Context <arch-callback-context>`.
Server
~~~~~~
The server side of ``host/common/gatt.c`` registers services (``bt_gatt_service_register``), provides the standard attribute read helpers (``bt_gatt_attr_read`` and the service, included-service, characteristic and CCC variants), the CCC write path (``bt_gatt_attr_write_ccc``, ``bt_gatts_sub_changed``) and the outbound notify and indicate API (``bt_gatt_notify_cb``, ``bt_gatt_indicate``). The behavior that differs most between hosts is how an inbound attribute access is completed, the exception first described in :ref:`Callback Execution Context <arch-callback-context>`:
- Bluedroid posts the read or write to the ISO task; the registered attribute callback runs there, and the response is returned to the peer through BTA asynchronously, coordinated with a dedicated server semaphore.
- NimBLE runs the attribute callback inline on the host task, because its access callback must return the value synchronously.
Outbound transfers follow the host's model as well. On NimBLE a notification is a synchronous buffer copy and dispatch, while an indication -- which must await the peer's confirmation -- is queued through NRP and completed when the confirmation arrives. On Bluedroid both go through BTA. In every case the payload handed to notify or indicate is copied before the call returns, so callers may reuse their buffers immediately.
.. _arch-iso-mtu:
MTU Exchange
~~~~~~~~~~~~
The ATT MTU exchange lets the two peers raise the ATT MTU above its 23-octet default so a single PDU can carry a larger attribute value. It is a GATT procedure: the **client** sends the ATT Exchange MTU Request and the **server** responds, and the smaller of the two peers' preferred values becomes the MTU in force. These GATT roles are independent of the GAP central/peripheral role -- a device usually runs both a client and a server regardless of which side opened the connection -- so the behavior is best read per GATT role.
**As a GATT server.** Both hosts always respond to the peer's ATT Exchange MTU Request and accept the negotiated value. This needs nothing from the application and is independent of the GAP role.
**As a GATT client.** Whether the device raises the MTU itself depends on the host stack and, on Bluedroid, on the GAP role:
- On Bluedroid it is automatic: the GATTC adapter issues the exchange when a GATT client connection opens (``handle_gattc_open_event`` calls ``BTA_GATTC_ConfigureMTU``, inside the ``BTA_GATTC_Enh_Open`` path). In practice this fires for a **central**; for a **peripheral** the GATT-client open happens inside the discovery-start path, which is itself gated behind the MTU-updated event -- a circular dependency that keeps it from initiating.
- On NimBLE the component initiates nothing, so the application triggers it with ``ble_gattc_exchange_mtu()`` -- typically right after the security-change event. On Bluedroid that same call is a no-op for the MTU, since the adapter has already exchanged it.
Because a GATT server only ever responds, a single exchange -- initiated by whichever side acts as client first -- settles the MTU for the whole connection; a peer that is also a client on that connection reuses the negotiated value rather than running a second exchange. The preferred value a device offers, and the reason a profile may raise it above the ATT default, are set by the layer above the transport: for Bluetooth LE Audio see :ref:`ATT MTU <arch-audio-mtu>`.
.. _arch-l2cap:
L2CAP (Draft)
-------------
.. warning::
L2CAP connection-oriented channels are an **early draft and are not yet officially supported**. The implementation is incomplete -- it exists only on NimBLE (``host/adapter/nimble/l2cap.c``) and there is **no Bluedroid support** -- and its API and behavior are provisional and may change. Do not rely on this layer in production.
``host/common/l2cap.c`` provides credit-based L2CAP connection-oriented channels: a server registry (``bt_l2cap_server_register``), the outbound channel operations (``bt_l2cap_chan_connect``, ``bt_l2cap_chan_disconnect``, ``bt_l2cap_chan_send``) and the inbound event handlers the host calls when a channel is accepted, connected, disconnected or receives data (``bt_le_l2cap_accept``, ``bt_le_l2cap_connected``, ``bt_le_l2cap_disconnected``, ``bt_le_l2cap_received``). The only consumer in the stack is the Object Transfer Service, which moves bulk objects over a credit-based channel; profiles that do not use object transfer never open one.
.. _arch-app-event:
Application Event Interface
---------------------------
GAP and GATT events do not reach the application straight from the adapter. They pass through a thin application-event interface in ``host/common/app/gap.c`` and ``host/common/app/gatt.c`` -- the single point at which GAP and GATT events leave and enter the application. The paragraphs below follow an inbound event from the adapter up to the application callback, then the reverse injection path.
**Registration.** The application registers exactly one callback per category, through ``bt_le_gap_app_cb_register`` and ``bt_le_gatt_app_cb_register``. Each category keeps a single callback pointer, so there is one application sink for all GAP events and one for all GATT events.
**Sourcing -- adapter to ISO task.** Before the interface can deliver an event, the adapter sources it from the host and posts it to the ISO task queue. For GAP, the Bluedroid adapter registers a BTA/BTM GAP callback in ``bt_le_bluedroid_gap_init``; the callback runs on the BTU task, builds an event, and enqueues it with ``bt_le_bluedroid_gap_post_event``. The NimBLE adapter supplies a ``ble_gap_event`` callback whose ``*_cb_safe`` wrapper takes the ISO lock and posts the event. GATT events are sourced the same way by the GATT adapters, detailed under GATT above. GAP has no engine of its own beyond this -- it is only an *event surface* over state that other modules own: the ACL connection table in ``conn.c`` (ACL connect and disconnect, security and identity changes, bond deletion) and the scanning and periodic-advertising-sync state in ``scan.c`` (extended-scan reports, PA-sync establish, lost and report, BIGInfo).
**Delivery -- ISO task to application.** When the ISO task dequeues a GAP or GATT event it calls ``bt_le_gap_handle_event`` or ``bt_le_gatt_handle_event`` (from ``task.c``). These dispatch on the event type, build a typed ``bt_le_gap_app_event`` or ``bt_le_gatt_app_event``, invoke the registered callback, and free the queued payload. Because this runs inside the ISO task, the application callback executes in the ISO task context under the ISO lock -- the context described in :ref:`Callback Execution Context <arch-callback-context>`. Delivered GAP events include ACL connect and disconnect, security and identity changes, bond deletion, extended-scan reports, periodic-advertising-sync state and BIGInfo; delivered GATT events include the ATT MTU change, GATT-client discovery completion and GATT-server subscription changes.
**Injection -- application to ISO task.** The reverse direction lets the application post an event into the ISO task, through ``bt_le_gap_app_post_event`` (exposed publicly as ``esp_ble_iso_gap_app_post_event``). It is host-divergent: on Bluedroid it forwards to ``bt_le_bluedroid_gap_post_event``, on NimBLE to ``bt_le_nimble_gap_post_event``. In the configuration these components ship, the injection path is used only on NimBLE.
.. mermaid::
flowchart TB
ADP["Adapter<br/>(host events)"]
T["ISO task queue"]
AIF["app/ interface<br/>(gap.c, gatt.c)"]
APP["Application<br/>(one GAP and<br/>one GATT callback)"]
ADP -->|post raw event| T
T -->|dequeue and dispatch| AIF
AIF -->|typed app event| APP
APP -->|app_post_event| AIF
AIF -->|inject into queue| T
**Why the interface is shaped this way.** Three goals drive this design:
- **One event model regardless of host.** The adapter normalizes every native event -- a BTA/BTM callback on Bluedroid, a ``ble_gap_event`` on NimBLE -- into the same typed ``bt_le_gap_app_event`` / ``bt_le_gatt_app_event`` before it reaches the application. The upper layers -- the examples and the audio profiles -- therefore handle an identical set of events and never branch on the host stack. This is the most important property of the interface: moving an application between the two hosts needs no change to its event handling.
- **Internal consumers get the event too.** Several events are not only for the application: the ISO engine in ``host/iso/iso.c`` and the prebuilt audio library consume them as well -- a periodic-advertising sync and its BIGInfo drive a BIG sync, and connect, disconnect and security changes drive the profiles. The interface therefore guarantees a copy reaches the ISO task, where that internal work runs under the ISO lock. On Bluedroid the BTU-task callback posts it directly; on NimBLE the application forwards the event it received into the ISO task with ``esp_ble_iso_gap_app_post_event`` (see :ref:`Per-Host Integration Differences <arch-iso-host-diff>`).
- **Coexistence with an ordinary BLE application.** Posting a copy to the ISO task does not consume the event. On Bluedroid the same callback still forwards the native event to the Bluedroid application-callback layer (BTC), so an application that also registered through ``esp_ble_gap_register_callback`` keeps receiving it for its own, non-audio BLE work; on NimBLE the application is already inside its ``ble_gap_event`` callback and can go on to handle whatever else it needs there. An ESP-IDF Bluetooth LE Audio application and a conventional BLE application can run side by side on one device.
.. important::
These application and profile callbacks run **on the ISO task** (see :ref:`Callback Execution Context <arch-callback-context>`), so a callback must return quickly. Blocking, sleeping, waiting on I/O, or running a long computation in one stalls the single event loop that every other GAP, GATT and -- most critically -- ISO data event depends on, which surfaces directly as late or dropped audio. Hand any lengthy work off to another task.
.. _arch-iso-api:
Public API
----------
Everything described so far is internal. An application sees only one public header, ``api/include/esp_ble_iso_common_api.h``, which exposes the transport as a small ``esp_ble_iso_*`` API for ISO-only use cases -- CIS or BIS without the Bluetooth LE Audio profiles.
Shape and Conventions
~~~~~~~~~~~~~~~~~~~~~
The public API is a thin facade over the ISO engine of the :ref:`transport layer <arch-iso-transport>`:
- **Opaque types.** Public types such as ``esp_ble_iso_chan_t``, ``esp_ble_iso_cig_t`` and ``esp_ble_iso_big_t`` are typedefs of the internal ``bt_iso_*`` structures, and ``esp_ble_conn_t`` is a typedef of ``bt_conn``. The application holds them as opaque handles.
- **Error codes.** Every function returns ``esp_err_t`` rather than the negative ``errno`` values the engine uses internally.
- **Lock on entry.** Each call enters the engine through its ``_safe`` wrapper, so the public API is the outermost point at which the global ISO lock is taken; the application never manages the lock itself.
- **One init.** ``esp_ble_iso_common_init`` takes an ``esp_ble_iso_init_info_t`` whose only field is the application's GAP callback; ISO events are then delivered through that callback or through the per-channel operations.
Functional Groups
~~~~~~~~~~~~~~~~~
.. list-table:: ESP-BLE-ISO public API by purpose
:header-rows: 1
:widths: 24 42 34
* - Group
- Representative functions
- Purpose
* - Initialization
- ``esp_ble_iso_common_init``
- Register the GAP callback and bring up the transport.
* - CIS -- central
- ``esp_ble_iso_cig_create``,\ |br|\ ``esp_ble_iso_cig_reconfigure``,\ |br|\ ``esp_ble_iso_cig_terminate``,\ |br|\ ``esp_ble_iso_chan_connect``
- Configure a Connected Isochronous Group and establish its streams.
* - CIS -- peripheral
- ``esp_ble_iso_server_register``,\ |br|\ ``esp_ble_iso_server_unregister``
- Accept incoming Connected Isochronous Streams.
* - BIG -- broadcaster
- ``esp_ble_iso_big_ext_adv_add``,\ |br|\ ``esp_ble_iso_big_create``,\ |br|\ ``esp_ble_iso_big_terminate``,\ |br|\ ``esp_ble_iso_big_register_cb``
- Broadcast an Isochronous Group over an advertising set.
* - BIG -- receiver
- ``esp_ble_iso_big_sync``
- Synchronize to a broadcast Isochronous Group.
* - Data path
- ``esp_ble_iso_setup_data_path``,\ |br|\ ``esp_ble_iso_remove_data_path``,\ |br|\ ``esp_ble_iso_chan_send``,\ |br|\ ``esp_ble_iso_chan_send_ts``
- Bind a stream to the controller's ISO data path and move SDUs.
* - Information
- ``esp_ble_iso_chan_get_info``,\ |br|\ ``esp_ble_iso_chan_get_tx_sync``
- Query channel and transmit-timing information.
* - Helpers
- ``esp_ble_iso_data_parse``
- Parse length-type-value (LTV) encoded data.
These map one-to-one onto the engine operations in the :ref:`ISO subsystem <arch-iso>`; the public layer adds only the lock, the error translation and the opaque typedefs.
.. _arch-iso-host-diff:
Per-Host Integration Differences
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Two parts of the public API exist specifically because the hosts route connection and GAP events differently. Both solve the same problem -- letting the engine see the events for a connection the application initiated -- in the way each host requires:
- ``esp_ble_iso_gap_app_post_event`` is **needed only on NimBLE**. When the application initiates a connection or scan on NimBLE, the host delivers GAP events to the callback the application registered; the application forwards them into the engine with this function. On Bluedroid the engine installs its own BTM GAP callback and captures the events directly, so no forwarding is required.
- ``esp_ble_iso_bluedroid_get_gattc_if`` is **Bluedroid only**. It returns the engine's internal BTA GATTC interface, which the application passes to ``esp_ble_gattc_open`` when initiating a connection so the resulting ACL events route back to the engine, avoiding a second BTA GATTC registration. The NimBLE counterpart is the event forwarding above.
@@ -0,0 +1,508 @@
.. _arch-audio:
ESP-BLE-AUDIO
=============
:link_to_translation:`zh_CN:[中文]`
.. |br| raw:: html
<br/>
.. contents:: Table of Contents
ESP-BLE-AUDIO sits directly on top of ESP-BLE-ISO and implements the Bluetooth LE Audio profiles and services. It is the layer an ESP-IDF Bluetooth LE Audio application actually programs against, through the ``esp_ble_audio_*`` API.
Component Layout
----------------
The component has two distinct kinds of code, and the distinction matters for this document:
- **Open source, covered here.** The public API headers (``api/include/esp_ble_audio_*_api.h``), the GATT *service* adapters under ``host/adapter/bluedroid/profiles`` and ``host/adapter/nimble/profiles``, the Object Transfer Service under ``host/services/ots``, and the initialization orchestration in ``host/common/init.c``.
- **Prebuilt, out of scope.** The ESP-IDF Bluetooth LE Audio profile and controller *logic* -- the unicast and broadcast clients, the volume, microphone, media and call control, set coordination, and the top-level profiles -- ships as a prebuilt per-target library (``lib/lib/<target>/libble_audio.a``, linked with ``add_prebuilt_library``). This document does not describe its internals; it describes the public contract around it and, where it matters, the task a registered callback runs on.
Layering
--------
.. mermaid::
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 40, 'subGraphTitleMargin': {'top': 6, 'bottom': 14}}}}%%
flowchart TB
APP["Application"]
API["ESP-BLE-AUDIO public API<br/>(esp_ble_audio_*)"]
subgraph AUDIO["ESP-BLE-AUDIO"]
direction LR
LIB["Prebuilt profile and<br/>controller library"]
SVC["GATT service adapters<br/>(per host)"]
OTS["Object Transfer Service"]
LIB ~~~ SVC
LIB ~~~ OTS
end
ISO["ESP-BLE-ISO transport<br/>(GATT, GAP, ISO, L2CAP, ISO task)"]
APP --> API --> AUDIO --> ISO
.. _arch-audio-svc-ctrl:
Services and Controllers
------------------------
Bluetooth LE Audio functionality divides into two kinds of building block, and the component reflects the split:
- **GATT services** are the attribute tables a peer reads, writes and subscribes to. They are implemented in the open per-host adapters (``host/adapter/*/profiles``), one source file per service, on top of the ESP-BLE-ISO GATT-server layer. Grouped by their profile, as in the Bluetooth LE Audio specification, the set is PACS, ASCS and BASS (BAP), MCS (MCP), TBS (CCP), CSIS (CSIP), MICS (MICP), VCS (VCP), CAS (CAP), TMAS (TMAP) and HAS (HAP).
- **Profile clients and controllers** are the state machines that drive those services and orchestrate streams: the Basic Audio and Common Audio profiles, volume, microphone, media and call control, coordinated-set identification, and the top-level Telephony-and-Media, Gaming, and Public-Broadcast profiles. This logic lives in the prebuilt library; the application reaches it through the matching ``esp_ble_audio_*_api.h`` header.
The GATT services are detailed in :ref:`GATT Services <arch-audio-services>`, and the clients and controllers in :ref:`Clients and Controllers <arch-audio-profiles>`.
Where Profile Callbacks Run
---------------------------
A registered ESP-IDF Bluetooth LE Audio callback runs on whichever task delivered the underlying transport event, so the rules from :ref:`Callback Execution Context <arch-callback-context>` carry over directly:
- A callback driven by a **notification** -- for example an ASCS or volume-state change arriving as a GATT notification -- runs on the ISO task on both hosts.
- A callback driven by a **GATT client read or write completion** -- for example reading a peer's PAC records -- runs on the ISO task on Bluedroid but on the **NimBLE host task** on NimBLE.
- A callback driven by an inbound **attribute write to a local service** -- for example a client configuring an ASE -- runs on the ISO task on Bluedroid but on the **NimBLE host task** on NimBLE.
The consequence is the one that runs through the whole stack: on the ISO task a callback shares the 4 KB stack and must not block the event loop, while on the NimBLE host task it executes inside the host's attribute path. In every case the global ISO lock is held, so profile state stays consistent regardless of which task the callback runs on.
.. _arch-audio-services:
GATT Services
-------------
Bluetooth LE Audio exposes its capabilities and state through a fixed set of GATT services. These are the *server* side of the profiles -- the attribute tables a peer reads, writes and subscribes to -- and, as established in :ref:`Services and Controllers <arch-audio-svc-ctrl>`, they are the open part of ESP-BLE-AUDIO: one source file per service under ``host/adapter/bluedroid/profiles`` and ``host/adapter/nimble/profiles``. The state behind each service, and the command handling for its control points, lives in the prebuilt library; this section covers the services and their adapters, not that logic.
.. _arch-audio-adapter:
Service Adapter Pattern
~~~~~~~~~~~~~~~~~~~~~~~
Every service is realized the same way. The prebuilt library owns the canonical, host-agnostic definition of the service's attribute table (a ``bt_gatt_service``); the open adapter registers that table with the active host's GATT server and then reconciles the handles the host assigns back onto the library's definition, so the library can address its own attributes when it notifies or indicates. The two hosts differ in how the registration is done:
- The **NimBLE** adapter declares the service a second time in NimBLE's native form (a ``ble_gatt_svc_def``), registers it with ``ble_gatts_add_svcs``, checks that its characteristics match the library's definition, and maps the assigned handles back with a ``*_attr_handle_set`` step. Registration is synchronous and single-phase.
- The **Bluedroid** adapter hands the library's ``bt_gatt_service`` to a shared ``bt_le_bluedroid_svc_init`` helper that drives BTA GATTS. Because BTA registers a service in two asynchronous phases, every Bluedroid service adapter exposes a matching ``*_init`` and ``*_start`` pair and coordinates completion with the server semaphore from the GATT-server layer.
Once a service is registered, peer access to its attributes follows the per-host attribute-access path of :ref:`Callback Execution Context <arch-callback-context>`: completed synchronously on the NimBLE host task, or posted to the ISO task on Bluedroid.
.. _arch-audio-deferred-add:
**When a service is added (deferred registration)**
**Mechanism.** Services are not all added to the GATT table during ``esp_ble_audio_common_init``. The build defines ``BLE_AUDIO_SVC_DEFERRED_ADD`` by default, which holds a service back until the application registers the matching role through its ``esp_ble_audio_<profile>_register`` call -- the prebuilt library turns that call into the service's ``bt_le_<service>_init`` registration -- rather than adding it during init.
**Reason.** A firmware image usually has more Bluetooth LE Audio capabilities compiled in than any one application uses: enabling a service's Kconfig role option builds its code in, but the application may never take that role. If every compiled-in service were added at init, a connecting peer would discover and could access attributes for a service that has no backing state behind it yet -- the application has not registered it, so the prebuilt library cannot answer for it meaningfully.
**Impact on the application.** Deferring the add keeps an unused-but-compiled-in capability out of the GATT table until the application opts into it, so a peer only ever discovers services that are fully backed.
Service Reference
~~~~~~~~~~~~~~~~~
.. list-table:: Bluetooth LE Audio GATT services
:header-rows: 1
:widths: 10 24 38 11 17
* - Service
- Full name
- Key characteristics
- Adapter
- Profile
* - PACS
- Published Audio Capabilities Service
- Sink and Source PAC records, audio locations, available and supported audio contexts.
- ``pacs.c``
- BAP
* - ASCS
- Audio Stream Control Service
- Sink and Source Audio Stream Endpoints (ASEs) and the ASE control point.
- ``ascs.c``
- BAP
* - BASS
- Broadcast Audio Scan Service
- Broadcast Receive State and the broadcast audio scan control point.
- ``bass.c``
- BAP
* - MCS
- Media Control Service
- Media player name and track information, the media control point and related state.
- ``mcs.c``
- MCP
* - TBS
- Telephone Bearer Service
- Bearer information, call states and the call control point.
- ``tbs.c``
- CCP
* - CSIS
- Coordinated Set Identification Service
- Set Identity Resolving Key (SIRK), set size, set member lock and rank.
- ``csis.c``
- CSIP
* - MICS
- Microphone Control Service
- Microphone mute; includes AICS.
- ``mics.c``
- MICP
* - VCS
- Volume Control Service
- Volume state, the volume control point and volume flags; includes VOCS and AICS.
- ``vcs.c``
- VCP
* - CAS
- Common Audio Service
- Identifies a Common Audio device; may include CSIS.
- ``cas.c``
- CAP
* - TMAS
- Telephony and Media Audio Service
- The device's TMAP role.
- ``tmas.c``
- TMAP
* - HAS
- Hearing Access Service
- Hearing-aid features, the preset control point and the active preset index.
- ``has.c``
- HAP
Each service is configured by the application through its profile's ``esp_ble_audio_*_api.h`` header (for example ``esp_ble_audio_vcp_api.h`` for VCS, ``esp_ble_audio_pacs_api.h`` for PACS); those APIs are part of the client-and-controller reference that follows.
Included Services and Control Points
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Several services are not standalone but are *included* by another service:
- **CAS** includes **CSIS** when the device is a member of a coordinated set, so a peer discovers set membership through the common audio device.
- **VCS** includes zero or more **VOCS** (Volume Offset Control Service) and **AICS** (Audio Input Control Service) instances -- one VOCS per independently offset output, one AICS per audio input.
- **MICS** includes zero or more **AICS** instances, one per audio input.
AICS and VOCS therefore have public API headers (``esp_ble_audio_aics_api.h`` and ``esp_ble_audio_vocs_api.h``) but no standalone adapter file; the including service registers them as part of its own attribute table.
Several services -- ASCS, BASS, MCS, TBS, CSIS, VCS and HAS -- expose a *control point*: a writable characteristic that carries a command and is answered by notifying the affected state characteristic. The command handling lives in the prebuilt library; the adapter only carries the write inward and the resulting notification outward, on the task described in :ref:`Callback Execution Context <arch-callback-context>`.
.. _arch-audio-profiles:
Clients and Controllers
-----------------------
The profiles and their client and controller roles are the part of ESP-BLE-AUDIO that ships in the prebuilt library. This section describes them at the level this document can: their roles, the public API through which an application drives them, and the task their callbacks run on. The internal state machines are out of scope.
A *client* or *controller* role acts on a remote device over a connection -- for example a volume controller changing a renderer's volume -- while a *renderer*, *device*, *member* or *server* role responds locally and is backed by one of the GATT services in the previous section. A single application can hold several roles at once.
Profile Relationships
~~~~~~~~~~~~~~~~~~~~~
Bluetooth LE Audio profiles build on one another. The **Basic Audio Profile (BAP)** is the foundation: it establishes and carries the audio streams, both unicast (over Connected Isochronous Streams) and broadcast (over Broadcast Isochronous Streams). The **control profiles** -- volume, microphone, coordinated-set, media and call control -- manage features and are each backed by a GATT service. The **Common Audio Profile (CAP)** coordinates the others so an operation applies consistently across every member of a set. The **top-level profiles** -- Telephony and Media Audio (TMAP), Gaming Audio (GMAP), Public Broadcast (PBP) and Hearing Access (HAP) -- are defined combinations of the layers below.
.. mermaid::
flowchart TB
TOP["Top-level profiles — TMAP, GMAP, PBP, HAP"]
CAP["CAP — coordinated control across a set"]
CTRL["Control profiles — VCP, MICP, CSIP, MCP, CCP"]
BAP["BAP — unicast (CIS) and broadcast (BIS) streams"]
SVC["Bluetooth LE Audio GATT services"]
ISO["ESP-BLE-ISO transport"]
TOP --> CAP
CAP --> BAP
CAP --> CTRL
TOP --> CTRL
CTRL --> SVC
BAP --> ISO
Profile Reference
~~~~~~~~~~~~~~~~~
.. list-table:: Bluetooth LE Audio profiles
:header-rows: 1
:widths: 10 27 45 18
* - Profile
- Full name
- Roles
- API header
* - BAP
- Basic Audio Profile
- Unicast client and server, broadcast source and sink, broadcast assistant, scan delegator.
- ``bap_api.h``
* - CAP
- Common Audio Profile
- Initiator, commander and handover across a coordinated set.
- ``cap_api.h``
* - VCP
- Volume Control Profile
- Volume controller (client) and volume renderer (server).
- ``vcp_api.h``
* - MICP
- Microphone Control Profile
- Microphone controller (client) and microphone device (server).
- ``micp_api.h``
* - CSIP
- Coordinated Set Identification Profile
- Set coordinator (client) and set member (server).
- ``csip_api.h``
* - MCP
- Media Control Profile
- Media Control Client (client) and the media proxy (server).
- ``mcc_api.h``, ``media_proxy_api.h``
* - CCP
- Call Control Profile
- Call Control Client (the server side is the Telephone Bearer Service).
- ``ccp_api.h``
* - TMAP
- Telephony and Media Audio Profile
- Role configuration combining CAP, BAP and the control profiles.
- ``tmap_api.h``
* - GMAP
- Gaming Audio Profile
- Role configuration for low-latency gaming audio.
- ``gmap_api.h``
* - PBP
- Public Broadcast Profile
- Public broadcast announcement helpers over BAP broadcast.
- ``pbp_api.h``
API Shape and Roles
~~~~~~~~~~~~~~~~~~~
Each profile exposes a small, uniform API in its ``esp_ble_audio_<profile>_api.h`` header: the application registers a callback structure for the role it is taking and calls functions to start operations -- discover a peer, configure a stream, set a volume, place a call. Results and state changes then arrive asynchronously through that callback, on the task described in :ref:`Callback Execution Context <arch-callback-context>` -- the ISO task in most cases, the NimBLE host task for GATT read and write completions and for inbound writes to a local service.
Because registration and dispatch are uniform across profiles, adding a role to an application always has the same shape: enable the role's Kconfig option, register its callback structure, and drive it through its functions. The role options also determine which GATT services are compiled in, as the initialization flow below shows.
Object Transfer (Draft)
-----------------------
.. warning::
Object Transfer is an **early draft and is not yet officially supported**. It builds on the draft L2CAP channel (:ref:`L2CAP <arch-l2cap>`), so it runs only on NimBLE and **not on Bluedroid**, and its API and behavior are provisional and may change. Do not rely on it in production.
The Object Transfer Service (OTS) moves bulk objects -- larger than an ordinary GATT read can carry -- over the credit-based L2CAP channel from :ref:`L2CAP <arch-l2cap>`. In Bluetooth LE Audio it is used by Media Control: a media player exposes objects such as the current track segmentation and group structure, and a client transfers them with OTS, reached through the media API (``esp_ble_audio_mcs_get_ots`` and the Media Control Client).
Unlike the profiles, OTS is open source, under ``host/services/ots``. Its parts are:
- a **server** (``ots.c``) and a **client** (``ots_client.c``);
- the **Object Action Control Point** (``ots_oacp.c``), which carries the read, write, create and similar object operations;
- the **Object List Control Point** (``ots_olcp.c``), which navigates the list of objects;
- an **object manager** (``ots_obj_manager.c``) and a **directory listing** object (``ots_dir_list.c``);
- the **L2CAP transport** (``ots_l2cap.c``) that carries object data over the credit-based channel.
.. _arch-init-flow:
Initialization Flow
-------------------
An ESP-IDF Bluetooth LE Audio application brings the stack up with two calls, mirroring the transport: ``esp_ble_audio_common_init`` followed by ``esp_ble_audio_common_start``.
``esp_ble_audio_common_init`` runs ``bt_le_audio_init`` in ``init.c``, which first establishes the boundary with the prebuilt library -- it checks that the shared-structure ABI matches and pushes the active Kconfig values into the library -- and then calls the host-specific ``bt_le_{host}_audio_init``. The host initializer brings up the standard GAP and GATT services and then registers each enabled Bluetooth LE Audio service through its adapter, in a fixed order: PACS, ASCS, BASS, TMAS, GTBS, HAS, CSIS and CAS, then the media, volume and microphone services. Each service is compiled in only when its Kconfig role option is set, so a build contains exactly the services its roles need. That fixed order is the order the services register in; *when* most of them actually enter the GATT table is a separate question -- by default the add is deferred to the application's per-role ``esp_ble_audio_<profile>_register`` call rather than happening here at init (see :ref:`deferred registration <arch-audio-deferred-add>`).
``esp_ble_audio_common_start`` runs ``bt_le_audio_start`` and dispatches to the host-specific start, which first registers the coordinated-set services (CSIS and CAS) from ``start_info`` and then commits the GATT server. On Bluedroid the commit is the second phase that actually starts the BTA services registered during init -- the ``*_init`` and ``*_start`` pairing from :ref:`The Service Adapter Pattern <arch-audio-adapter>`; on NimBLE it calls ``ble_gatts_start`` and then reconciles the assigned attribute handles back into the library.
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant APP as Application
participant CMN as ESP-BLE-AUDIO (init.c)
participant LIB as Prebuilt library
participant AD as Host adapter
APP->>CMN: esp_ble_audio_common_init (gap_cb, gatt_cb)
CMN->>LIB: check ABI, push configuration
CMN->>AD: bt_le_{host}_audio_init
Note over AD: bring up GAP/GATT, then register<br/>enabled services in fixed order
APP->>CMN: esp_ble_audio_common_start
CMN->>AD: bt_le_{host}_audio_start
Note over AD: register CSIS/CAS from start_info, then commit the GATT server<br/>Bluedroid: phase-2 start of the registered BTA services<br/>NimBLE: ble_gatts_start, then reconcile attr handles
.. _arch-audio-mtu:
ATT MTU
~~~~~~~
Bluetooth LE Audio depends on an ATT MTU large enough to carry its control PDUs.
**Spec floor.** The Basic Audio Profile (BAP) requires a minimum ATT MTU of only 64 octets (the public ``ESP_BLE_AUDIO_ATT_MTU_MIN``).
**Why 128.** 64 octets is not enough to operate four ASEs at once, so ESP-IDF raises the default to 128 octets (``BLE_AUDIO_ATT_MTU_MIN``) to leave headroom for the ASCS and PACS control PDUs that Bluetooth LE Audio relies on. Both hosts set the device's preferred ATT MTU to this value during initialization (``bt_le_{host}_audio_init``), through ``BTA_GATT_SetLocalMTU`` on Bluedroid and ``ble_att_set_preferred_mtu`` on NimBLE.
**Negotiation rule.** This is the value the device offers as a client and accepts as a server, so the MTU that ends up in force is the smaller of the two peers' preferred values.
The exchange itself is the GATT procedure described in :ref:`MTU Exchange <arch-iso-mtu>`: the client sends the request, the server responds, and one negotiation governs the whole connection. In the common Bluetooth LE Audio topology -- a phone as central and an ESP device as peripheral -- each device runs **both** a GATT client and a GATT server, since the GATT roles are independent of the GAP role. Yet a single MTU exchange covers the whole connection: the phone's client initiates it, the ESP's server responds, and the one negotiated value then governs all ATT traffic between them -- no matter which side is acting as client or server at a given moment:
.. mermaid::
sequenceDiagram
box transparent Phone (GAP central)
participant PS as GATT server
participant PC as GATT client
end
box transparent ESP (GAP peripheral)
participant ES as GATT server
participant EC as GATT client
end
PC->>ES: ATT Exchange MTU Request
ES-->>PC: ATT Exchange MTU Response
Note over ES: server responds, never initiates
Note over PS,EC: the one negotiated MTU (min of both sides)<br/>governs all ATT traffic, both directions
PC->>ES: service discovery
PC->>ES: subscribe (CCCD), then PACS / ASCS reads, writes
EC->>PS: service discovery (peripheral as client, reuses the same MTU)
The other two roles -- the phone's GATT server and the ESP's GATT client -- are present on the same connection: the ESP's client can run its own service discovery against the phone's server, but neither role runs a second MTU exchange; both reuse the value the phone's client already negotiated. Because the ESP peripheral only responds and never initiates the MTU exchange, that one negotiation is paced by the central, not by the peripheral's host stack.
End-to-End Flows
----------------
The four flows below tie the layers together for the two unicast roles and the two broadcast roles. They are deliberately high-level -- each arrow may stand for several exchanges -- and show which component owns each step.
Unicast Initiator
~~~~~~~~~~~~~~~~~
A unicast initiator connects to an acceptor, configures its audio stream endpoints, establishes a Connected Isochronous Stream and starts sending audio:
.. mermaid::
sequenceDiagram
participant APP as Application (initiator)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: scan and connect (GAP)
APP->>ISO: start GATT discovery
AUD->>ISO: discover PACS and ASCS, subscribe to ASEs
AUD->>ISO: configure ASEs (write the ASE control point)
AUD->>ISO: create CIG and connect CIS
ISO->>CTRL: LE Set CIG Parameters, Create CIS
CTRL->>ISO: CIS Established event
AUD->>ISO: set up the ISO data path (input)
APP->>CTRL: send ISO SDUs
CTRL->>ISO: ISO transmit complete
Note over APP,CTRL: subsequent SDUs repeat the send / transmit-complete cycle
The stream setup -- discovering PACS and ASCS, configuring the ASEs, creating the CIG, connecting the CIS and setting up the data path -- is driven by the ESP-BLE-AUDIO profile library on top of ESP-BLE-ISO's GATT and ISO layers, not by the application directly. The application drives only the high-level flow through the ``esp_ble_audio_*`` API -- connect, start discovery, start the stream and send audio -- and ESP-BLE-AUDIO carries out the underlying ESP-BLE-ISO operations on its behalf.
Unicast Acceptor
~~~~~~~~~~~~~~~~
A unicast acceptor advertises, accepts a connection from an initiator and lets the initiator configure its audio stream endpoints before receiving audio:
.. mermaid::
sequenceDiagram
participant APP as Application (acceptor)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: advertise (GAP, audio announcement)
CTRL->>ISO: ACL connection established (peripheral)
CTRL->>ISO: remote initiator writes the ASE control point
ISO->>AUD: ASCS server applies config / QoS / enable
AUD->>APP: config / QoS / enable callbacks
APP->>AUD: response (accept / reject + QoS preferences)
AUD->>ISO: notify ASE state and control-point result
ISO->>CTRL: send to the remote initiator
CTRL->>ISO: CIS Established event
AUD->>ISO: set up the ISO data path (output)
CTRL->>ISO: ISO SDUs
ISO->>APP: recv callback on the ISO task
Note over APP,CTRL: subsequent ISO SDUs repeat the receive / recv-callback cycle
Unlike the initiator, the acceptor is passive. The remote initiator drives the ASCS state machine by writing the ASE control point, and the acceptor's ESP-BLE-AUDIO ASCS server surfaces each step -- config, QoS, enable -- to the application as callbacks. Each callback returns the application's response -- accept or reject, plus QoS preferences at codec configuration -- which the ASCS server notifies back to the initiator as the updated ASE state and control-point result. The Connected Isochronous Stream is established by the initiator (the acceptor is the peripheral); ESP-BLE-AUDIO sets up the ISO data path and delivers the received SDUs to the application through the recv callback on the ISO task. As on the initiator, the application never calls the ESP-BLE-ISO ISO API directly.
Broadcast Source
~~~~~~~~~~~~~~~~
A broadcast source configures its audio, starts a periodic advertising train carrying the BASE and transmits audio into a Broadcast Isochronous Group without any connection:
.. mermaid::
sequenceDiagram
participant APP as Application (broadcast source)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>AUD: create broadcast source (codec, QoS, BASE)
APP->>ISO: start extended and periodic advertising (GAP, carries the BASE)
APP->>AUD: start the broadcast source
AUD->>ISO: create BIG
ISO->>CTRL: LE Create BIG
CTRL->>ISO: BIG Complete event
AUD->>ISO: set up the ISO data path (input)
AUD->>APP: stream started callback
APP->>CTRL: send ISO SDUs
CTRL->>ISO: ISO transmit complete
Note over APP,CTRL: subsequent SDUs repeat the send / transmit-complete cycle
The application drives the high-level flow through the ``esp_ble_audio_*`` API -- create the broadcast source, start advertising, start the source and send audio -- while ESP-BLE-AUDIO builds the BASE, creates the BIG on top of ESP-BLE-ISO's ISO subsystem and sets up the data path. As with the unicast initiator, the underlying ESP-BLE-ISO operations are carried out by ESP-BLE-AUDIO, not the application. A broadcast source has no connection and no GATT: it only advertises and transmits.
Broadcast Sink
~~~~~~~~~~~~~~
A broadcast sink synchronizes to a broadcaster's periodic train, joins its Broadcast Isochronous Group and receives audio:
.. mermaid::
sequenceDiagram
participant APP as Application (broadcast sink)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: scan and synchronize to periodic advertising
CTRL->>ISO: BIGInfo report
AUD->>ISO: BIG sync (with broadcast code if encrypted)
ISO->>CTRL: LE BIG Create Sync
CTRL->>ISO: BIG Sync Established event
AUD->>ISO: set up the ISO data path (output)
CTRL->>ISO: ISO SDUs
ISO->>APP: recv callback on the ISO task
Note over APP,CTRL: subsequent ISO SDUs repeat the receive / recv-callback cycle
The application drives the high level -- periodic-advertising synchronization (through GAP) and the broadcast-sink start -- while the ESP-BLE-AUDIO broadcast-sink profile performs the BIG sync and the data path. Both rest on ESP-BLE-ISO: its scanning layer for PA sync and the BIGInfo report, its ISO subsystem for the BIG sync and data path. If the group is encrypted and the broadcast code is wrong, the controller still synchronizes but the stream fails its message-integrity check shortly afterward, so an application should treat an early post-sync failure as a likely bad code.
.. _arch-audio-api:
Public API
----------
The application drives the ESP-IDF Bluetooth LE Audio implementation through the ``esp_ble_audio_*`` API. It has two surfaces: one common header, ``api/include/esp_ble_audio_common_api.h``, for bring-up and the cross-cutting entry points, and one ``<profile>_api.h`` header per profile and service for the role-specific operations -- the bulk of the API, cataloged in :ref:`Clients and Controllers <arch-audio-profiles>`.
Shape and Conventions
~~~~~~~~~~~~~~~~~~~~~
The common API sits on the ESP-BLE-ISO :ref:`transport <arch-iso-transport>` and the prebuilt profile library, and follows the same conventions as the transport's public API:
- **Two-call bring-up.** ``esp_ble_audio_common_init`` takes an ``esp_ble_audio_init_info_t`` -- a GAP callback and a GATT callback, the single application sinks of :ref:`Application Event Interface <arch-app-event>`. ``esp_ble_audio_common_start`` takes an ``esp_ble_audio_start_info_t`` carrying the coordinated-set (CSIS) service instances to start. What the two calls do internally is the subject of :ref:`Initialization Flow <arch-init-flow>`: a handshake with the prebuilt library -- the shared-structure ABI (Application Binary Interface) check and the Kconfig configuration push -- followed by the per-service registration.
- **Opaque, transport-aliased types.** The event types an application handles -- ``esp_ble_audio_gap_app_event_t`` and ``esp_ble_audio_gatt_app_event_t`` -- are typedefs of the transport's ``bt_le_gap_app_event`` / ``bt_le_gatt_app_event``, and the ``ESP_BLE_AUDIO_GAP_EVENT_*`` / ``ESP_BLE_AUDIO_GATT_EVENT_*`` codes alias the transport's, so the audio and ISO layers present one event model.
- **Error codes.** Every function returns ``esp_err_t``.
- **Per-role registration.** A profile role follows the uniform shape from :ref:`Clients and Controllers <arch-audio-profiles>`: enable the role's Kconfig option, register its callback structure through the role's ``<profile>_api.h``, and drive it through that header's functions. Registering a role is also what adds its GATT service to the table (see :ref:`deferred registration <arch-audio-deferred-add>`).
Functional Groups
~~~~~~~~~~~~~~~~~
.. list-table:: ESP-BLE-AUDIO public API by purpose
:header-rows: 1
:widths: 24 42 34
* - Group
- Representative functions
- Purpose
* - Initialization
- ``esp_ble_audio_common_init``
- Register the GAP and GATT callbacks and bring up the prebuilt library, GAP and GATT.
* - Start
- ``esp_ble_audio_common_start``
- Register the coordinated-set services (CSIS, CAS) from ``start_info`` and start the GATT server (the second BTA phase on Bluedroid).
* - GATT discovery
- ``esp_ble_audio_gattc_disc_start``
- Start GATT discovery of a peer's services on a connection.
* - Event forwarding
- ``esp_ble_audio_gap_app_post_event``,\ |br|\ ``esp_ble_audio_gatt_app_post_event``
- Forward host GAP and GATT events into the engine (NimBLE only -- see below).
* - LTV helpers
- ``esp_ble_audio_data_parse``,\ |br|\ ``esp_ble_audio_data_get_val``
- Parse the length-type-value metadata Bluetooth LE Audio uses throughout.
* - Per-profile roles
- the ``<profile>_api.h`` register and operation functions
- Take and drive a profile role -- the bulk of the API (see :ref:`Clients and Controllers <arch-audio-profiles>`).
Per-Host Integration Differences
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As with ESP-BLE-ISO, the only host-divergent entry points are the event-forwarding functions, and they exist for the same reason -- letting the engine see the GAP and GATT events for a connection the application drives:
- ``esp_ble_audio_gap_app_post_event`` and ``esp_ble_audio_gatt_app_post_event`` are **needed only on NimBLE**: the host delivers GAP and GATT events to the callbacks the application registered, and the application forwards them inward with these. On Bluedroid the adapter installs its own BTA/BTM callbacks and captures the events directly, so no forwarding is required -- ``esp_ble_audio_gatt_app_post_event`` is even hidden from Bluedroid builds, so calling it there is a compile-time error.
@@ -0,0 +1,284 @@
.. _ble-audio-architecture:
ESP-IDF Bluetooth LE Audio Architecture
=======================================
:link_to_translation:`zh_CN:[中文]`
This document describes the internal architecture of the ESP-IDF Bluetooth LE Audio stack together with the general-purpose BLE transport it is built on, across two Bluetooth components:
- **ESP-BLE-ISO** -- a general-purpose BLE *transport* layer. It provides the GATT, GAP, ISO, L2CAP and HCI primitives, together with the dedicated processing task and locking model that serialize all host events. It is **not specific to Bluetooth LE Audio** -- it is a self-contained transport that any upper-layer profile can build on, and that can also **run on its own**: it has its own public API (``esp_ble_iso_*``) and its own initialization, so an application can use it directly without ESP-BLE-AUDIO or any profile layer above it.
- **ESP-BLE-AUDIO** -- the upper *profile* layer. It implements the Bluetooth LE Audio profiles and services (PACS, ASCS, BASS, BAP, CAP, VCP, MICP, CSIP, MCP, CCP, TMAP, and so on) and exposes them through the public ``esp_ble_audio_*`` API.
.. note::
ESP-BLE-AUDIO is currently the only consumer of ESP-BLE-ISO, but it is not the only intended one, and it is not required at all.
ESP-BLE-ISO is an independent component: it can be built and initialized on its own and driven through its public ``esp_ble_iso_*`` API for ISO-only use (CIS or BIS without any profile layer) -- see :ref:`ESP-BLE-ISO Public API <arch-iso-api>`.
The transport is also profile-agnostic: its GATT, GAP, ISO, L2CAP and HCI primitives are general BLE building blocks, and other profiles -- for example a future HID-over-ISO -- are expected to build on the same component. Wherever this document describes ESP-BLE-ISO, the behavior applies to any consumer, not only to Bluetooth LE Audio.
Both components run on either of two BLE host stacks -- **Bluedroid** or **NimBLE** -- behind a common interface. Making the *behavioral differences* between the two hosts explicit is a primary goal of this document, because they determine the task context in which callbacks run, the order of operations during connection setup, and how events are dispatched.
.. note::
This guide serves two audiences. The *Overview* sections give a layer-level mental model usable by anyone integrating the public API of either component (:ref:`ESP-BLE-ISO <arch-iso-api>`, :ref:`ESP-BLE-AUDIO <arch-audio-api>`). The later sections descend to the implementation level (tasks, queues, locks, per-host dispatch paths) for maintainers working on the components themselves. Source is referenced by file path and symbol name rather than line number, so the references stay valid as the code evolves.
.. |br| raw:: html
<br/>
.. contents:: Table of Contents
:local:
:depth: 1
Architecture Overview
---------------------
Layered Stack
~~~~~~~~~~~~~
ESP-IDF Bluetooth LE Audio is organized as a stack of cooperating layers. Each layer talks only to the layer directly below it, and all host-specific knowledge is confined to the *adapter* sublayer inside each component.
.. mermaid::
flowchart TB
APP["Application / Examples (app task)"]
AUDIO["ESP-BLE-AUDIO —<br/>Bluetooth LE Audio profiles and services<br/>(esp_ble_audio_* API)"]
ISO["ESP-BLE-ISO — transport primitives:<br/>GATT, GAP, ISO, L2CAP, HCI<br/>all serialized by the global ISO lock —<br/>most events run on the ISO task event loop"]
HOST["BLE Host Stack<br/>(Bluedroid or NimBLE)"]
CTRL["BLE controller"]
APP <--> AUDIO
AUDIO <--> ISO
ISO <--> HOST
HOST <--> CTRL
ISO <-. ISO Data Path .-> CTRL
.. note::
The solid links are the control path, which follows the layering in both directions -- commands flow down, events flow up. The **ISO data path** is the exception (the dashed link): it bypasses the host stack in both of the Core Specification's data-path directions. On the **Input** data path (Host to Controller), ESP-BLE-ISO writes SDUs straight to the controller transport, and the controller reports transmit-complete back through a callback into the ISO task; on the **Output** data path (Controller to Host), the controller delivers received ISO data through the same kind of callback. Neither path goes through the host stack's HCI event dispatch, which keeps the high-rate audio data path short.
The application calls only the public ``esp_ble_audio_*`` API (or, for ISO-only use cases, ``esp_ble_iso_*``). Everything below the public API is internal and may change between releases.
Two Components
~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 16 40 44
* - Component
- Responsibilities
- Key directories
* - ESP-BLE-ISO
- - ACL connection management
- Advertising and scanning
- GATT client and server
- ISO (CIS and BIS)
- HCI and L2CAP
- ISO task event loop
- ``esp_ble_iso_*`` API
- - ``host/common``
- ``host/adapter/bluedroid``
- ``host/adapter/nimble``
- ``api/include``
* - ESP-BLE-AUDIO
- - Bluetooth LE Audio GATT services
- Profile clients and controllers
- Object Transfer Service (OTS)
- ``esp_ble_audio_*`` API
- - ``host/common``
- ``host/adapter/bluedroid/profiles``
- ``host/adapter/nimble/profiles``
- ``host/services/ots``
- ``api/include``
.. _arch-dual-host:
Dual-Host Design
----------------
Each component is split into a **host-agnostic common** part and a **host-specific adapter** part. The directory under ``host/common`` holds one implementation that does not depend on the host stack, while ``host/adapter/bluedroid`` and ``host/adapter/nimble`` hold the code that talks to each host directly. Exactly one adapter is compiled, selected by the active host (``CONFIG_BT_BLUEDROID_ENABLED`` or ``CONFIG_BT_NIMBLE_ENABLED``). The common layer defines the internal interface that both adapters implement, so the upper layers never branch on the host stack.
The two hosts are *not* behaviorally identical. They differ in the task their callbacks run on, in how an inbound event reaches the ISO task, and in a number of procedure-level details (MTU exchange, service discovery, ISO setup) that are covered in the per-layer sections. The structural differences are summarized below; the most important one for application developers -- the task a callback runs in -- is detailed in :ref:`Callback Execution Context <arch-callback-context>`.
.. list-table:: Host-level differences
:header-rows: 1
:widths: 26 37 37
* - Aspect
- Bluedroid
- NimBLE
* - Task that raises inbound events
- The BTU task (BTA GATT and GAP callbacks run here).
- The NimBLE host task.
* - How the adapter forwards an event
- A BTA callback builds an event and posts it to the ISO task queue.
- A ``*_cb_safe`` wrapper takes the ISO lock first; most events are then posted to the ISO task queue.
* - Synchronous exception
- GATT-server attribute access is completed asynchronously: the request is posted to the ISO task, and the response is sent later.
- GATT-server attribute access is completed inline on the host task, because NimBLE requires the value to be returned synchronously.
* - ISO task CPU core
- Pinned to the configured Bluedroid core.
- Pinned to the configured NimBLE core.
* - ISO task priority
- Matched to the BTU task priority.
- Matched to the NimBLE host task priority.
.. note::
The ISO task is deliberately given the same CPU core and the same priority as the host stack's main task (see ``host/common/include/common/task.h``). Running the transport task at the host's priority keeps the two cooperatively scheduled instead of preempting each other.
Component Internals
-------------------
The implementation details of each component are listed in the table below, covering tasks, queues, locks, and the per-host dispatch paths, for maintainers working inside the component:
.. list-table::
:header-rows: 1
:widths: 26 74
* - Page
- What it covers
* - :ref:`ESP-BLE-ISO <arch-iso-transport>`
- - concurrency model -- the single ISO task and global lock, and the task each callback runs on
- connection management
- advertising, scanning and periodic-advertising sync
- HCI command path
- ISO subsystem -- CIS and BIG state machines, and the SDU data path
- GATT client and server, and the ATT MTU exchange
- L2CAP (draft) and the application event interface
- ``esp_ble_iso_*`` public API
* - :ref:`ESP-BLE-AUDIO <arch-audio>`
- - open profile adapters and the prebuilt library
- GATT services and the service-adapter pattern
- profile clients and controllers
- object transfer (draft)
- initialization sequence and ATT-MTU sizing
- end-to-end unicast and broadcast flows
- ``esp_ble_audio_*`` public API
.. toctree::
:hidden:
:maxdepth: 1
ble-audio-architecture-iso
ble-audio-architecture-lea
Cross-Cutting Concerns
----------------------
This section collects the invariants and practical concerns that span both components and recur throughout the implementation.
Host Parity
~~~~~~~~~~~
The most important maintenance invariant is that the two host adapters stay behaviorally equivalent. The common layers above them assume identical behavior, so a change to one adapter's connection, cleanup, discovery or event path must be mirrored in the other -- otherwise the upper layers behave differently depending on the host. Where the two genuinely cannot match, the asymmetry is deliberate, and the complete set is small:
.. list-table:: Intentional per-host differences
:header-rows: 1
:widths: 30 35 35
* - Area
- Bluedroid
- NimBLE
* - Task that raises inbound events
- BTU task
- NimBLE host task
* - GATT-server attribute access
- Posted to the ISO task; response sent asynchronously through BTA
- Inline on the host task; value returned synchronously
* - GATT-client read/write completion
- Posted to the ISO task
- Inline on the host task
* - HCI ISO commands
- Private direct-HCI path with its own completion semaphore
- Typed ``ble_hs_hci_*`` helpers
* - GATT service registration
- Two-phase and asynchronous (``*_init`` then ``*_start``)
- Single-phase ``ble_gatts_add_svcs``
* - L2CAP connection-oriented channels
- Not implemented (TODO)
- Implemented (Draft)
* - Object Transfer Service (OTS)
- Not implemented (builds on L2CAP)
- Implemented (Draft)
* - Connection-initiation event routing
- Share the engine's BTA GATTC interface (``esp_ble_iso_bluedroid_get_gattc_if``)
- Forward GAP events into the engine (``*_gap_app_post_event``)
Any divergence beyond these needs the same kind of justification, recorded at the point where it is introduced.
Layer Boundaries
~~~~~~~~~~~~~~~~
The architecture depends on a few boundaries that callers and maintainers should respect:
- **The public API is the only stable surface.** Everything below the ``esp_ble_iso_*`` and ``esp_ble_audio_*`` API is internal and may change between releases.
- **The two BLE host stacks are adapted, not modified.** The adapter layers exist precisely so the components never change Bluedroid or NimBLE themselves; host behavior is taken as given.
- **The profile and controller logic is a prebuilt library with a fixed ABI.** The open code interacts with it only through the shared function and configuration tables that are validated at initialization; its internals are not part of this contract.
Common Pitfalls
~~~~~~~~~~~~~~~
The concurrency model in :ref:`Concurrency and Thread Safety <arch-concurrency>` has practical consequences that are easy to get wrong:
- **Do not block in a callback.** Most callbacks run on the ISO task; blocking there stalls every other event in the stack. Its stack is only 4 KB, so deep call chains and large stack-allocated buffers -- including arrays sized from configuration values -- must be avoided.
- **Posted events do not race each other.** Because a single task drains the ISO task queues, two events that were both posted cannot run concurrently; reasoning that assumes they can is almost always wrong.
- **Mind the callback's task on NimBLE.** A GATT read or write completion, or an inbound write to a local service, runs on the NimBLE host task rather than the ISO task; code shared between paths must be correct on both.
- **The ISO lock orders access, not completion.** Holding it guarantees mutual exclusion, not that an operation you started has finished -- many transport operations complete on a later event.
Memory Footprint
~~~~~~~~~~~~~~~~
The open ESP-BLE-ISO and ESP-BLE-AUDIO code has small, deterministic fixed costs:
- The ISO task uses a 4 KB stack and three priority event queues (32 + 64 + 32 entries) drained by one consumer.
- The ISO engine's channel, group and broadcast pools are sized by ``CONFIG_BT_ISO_MAX_CHAN``, ``CONFIG_BT_ISO_MAX_CIG`` and ``CONFIG_BT_ISO_MAX_BIG``; the HCI command pool holds a single buffer.
- Per connection, NimBLE caches the discovered attribute database and holds deep-copied parameters for GATT procedures in flight; subscriptions are tracked in per-connection lists on both hosts.
- GATT service attribute tables are allocated at initialization, sized by the services that are enabled.
Because these pools and tables are sized from Kconfig, the open code's footprint is fixed at build time, with no per-stream dynamic growth beyond the SDU buffers in flight. The prebuilt profile-and-controller library is the exception: it allocates its own profile and controller state dynamically -- at initialization and as connections and streams are set up -- so that part of the footprint is not fixed at build time.
Appendix -- File and Directory Map
----------------------------------
ESP-BLE-ISO
~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 34 66
* - Directory
- Contents
* - ``api/include``
- The public ``esp_ble_iso_*`` header.
* - ``host/common``
- Host-agnostic transport: connection, advertising, scanning, GATT, HCI, ISO glue, L2CAP, the ISO task and the ISO lock; ``app/`` holds the application GAP and GATT event surface.
* - ``host/iso``
- The ISO engine (CIS and BIG state machines and channel API).
* - ``host/adapter/bluedroid``,\ |br|\ ``host/adapter/nimble``
- Per-host adapters for GAP, GATT, ISO, HCI and (NimBLE) L2CAP.
* - ``host/utils``
- Address, UUID, CRC, crypto, timer and buffer helpers.
ESP-BLE-AUDIO
~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 34 66
* - Directory
- Contents
* - ``api/include``
- Public ``esp_ble_audio_*_api.h`` headers, one per profile and service.
* - ``host/common``
- Initialization orchestration (``init.c``).
* - ``host/adapter/bluedroid/profiles``,\ |br|\ ``host/adapter/nimble/profiles``
- The GATT service adapters (PACS, ASCS, BASS, CAS, CSIS, HAS, MCS, MICS, TBS, TMAS, VCS).
* - ``host/services/ots``
- The Object Transfer Service.
* - ``lib``
- The prebuilt per-target profile and controller library.
@@ -0,0 +1,83 @@
Feature Support Status
======================
:link_to_translation:`zh_CN:[中文]`
This page tracks the support status of Bluetooth LE Audio features in ESP-IDF — the Generic Audio Framework profiles and services provided by ESP-BLE-AUDIO, and the isochronous transport provided by ESP-BLE-ISO.
.. note::
The Bluetooth LE Audio features marked as supported below are currently a **preview**: their APIs and behavior are provisional and may change in future releases.
The table below lists the Bluetooth LE Audio profiles and services currently supported in ESP-IDF.
.. list-table::
:header-rows: 1
:widths: 28 12 60
* - Profile / Service
- Supported
- Notes
* - LE Isochronous Channels (CIS / BIS)
- Yes
- Direct ISO access via :doc:`ESP-BLE-ISO <../../api-reference/bluetooth/esp-ble-iso>`.
* - BAP
- Yes
- All six BAP roles: Unicast Client, Unicast Server, Broadcast Source, Broadcast Sink, Broadcast Assistant, Scan Delegator.
* - PACS
- Yes
- Used by BAP Unicast Server and Broadcast Sink.
* - ASCS
- Yes
- Used by BAP Unicast Server.
* - BASS
- Yes
- Used by BAP Scan Delegator and Broadcast Assistant.
* - CAP
- Yes
- All three CAP roles: Acceptor, Initiator, Commander.
* - CAS
- Yes
- Mandatory service on CAP Acceptors.
* - CSIP / CSIS
- Yes
- Set Member and Set Coordinator roles.
* - VCP / VCS
- Yes
- Volume Renderer and Volume Controller roles.
* - VOCS
- Yes
- Per-output volume offset control; included in VCS as an optional sub-service.
* - AICS
- Yes
- Audio input control; included in VCS and MICS as an optional sub-service.
* - MICP / MICS
- Yes
- Microphone Device and Microphone Controller roles.
* - MCP / MCS
- Partial
- Media Control Server and Media Control Client roles are supported. OTP/OTS-based media object transfer is not currently supported.
* - CCP / TBS
- Yes
- Call Control Server and Call Control Client roles, including GTBS and TBS.
* - HAP / HAS
- Yes
- Hearing Aid and Hearing Aid Unicast Client roles, including preset read/write via HAS.
* - TMAP / TMAS
- Yes
- All six TMAP roles: CG, CT, UMS, UMR, BMS, BMR.
* - GMAP / GMAS
- Yes
- All four GMAP roles: UGG, UGT, BGS, BGR.
* - PBP
- Yes
- Public Broadcast Source and Public Broadcast Sink roles.
* - OTP / OTS
- No
- Object Transfer Profile/Service (used by MCP/MCS for media object transfer) is not currently supported.
.. note::
For the profile and service definitions, see :doc:`Bluetooth LE Audio Standard <ble-audio-introduction>`.
For general Bluetooth Low Energy feature support, see :doc:`Major Feature Support Status <../ble/ble-feature-support-status>`.
@@ -0,0 +1,25 @@
ESP-IDF Bluetooth LE Audio
==========================
:link_to_translation:`zh_CN:[中文]`
Bluetooth LE Audio is the audio architecture introduced in Bluetooth Core Specification 5.2. When reading this documentation, it helps to keep two ideas separate:
- The **standard** is the set of profiles, services, and roles defined by the Bluetooth SIG — the Generic Audio Framework, layered on top of the LE isochronous transport. It is platform-independent and is described in :doc:`Bluetooth LE Audio Standard <ble-audio-introduction>`.
- The **implementation** is the software that realizes the standard on a particular platform. The ESP-IDF implementation is described in :doc:`ESP-IDF Bluetooth LE Audio Architecture <ble-audio-architecture-overview>`.
In ESP-IDF, the implementation is delivered as two components:
- **ESP-BLE-ISO** provides the isochronous transport — the Connected and Broadcast Isochronous Streams (CIS/BIS) that carry the audio. See its :doc:`API reference <../../api-reference/bluetooth/esp-ble-iso>`.
- **ESP-BLE-AUDIO** provides the Generic Audio Framework profiles and services, built on top of ESP-BLE-ISO. See its :doc:`API reference <../../api-reference/bluetooth/esp-ble-audio>`.
Both components run on either of the two ESP-IDF Bluetooth host stacks, **Bluedroid** or **NimBLE**. Their API and behavior are kept identical across the two hosts wherever possible; where a behavioral difference is unavoidable, it is called out in the architecture document together with the reason.
For the support status of individual features, see :doc:`Feature Support Status <ble-audio-feature-support-status>`.
.. toctree::
:maxdepth: 1
ble-audio-introduction
ble-audio-architecture-overview
ble-audio-feature-support-status
@@ -0,0 +1,466 @@
Bluetooth LE Audio Standard
===========================
:link_to_translation:`zh_CN:[中文]`
This document introduces the Bluetooth LE Audio specification: its profiles, services, the roles they define, and the dependency relationships among them. It is intended as a conceptual reference to help you choose the right set of profiles for your application before working with the :doc:`ESP-BLE-AUDIO API reference <../../api-reference/bluetooth/esp-ble-audio>`.
.. note::
This document covers the Bluetooth LE Audio **specification** — its profiles, services, roles, and dependencies. For the **ESP-IDF implementation** architecture (the ESP-BLE-ISO and ESP-BLE-AUDIO components, their task and locking model, and the per-host adapters), see :doc:`ESP-IDF Bluetooth LE Audio Architecture <ble-audio-architecture-overview>`.
Overview
--------
Bluetooth LE Audio, introduced in the Bluetooth Core Specification 5.2, enables high-quality, low-power audio over Bluetooth LE using the following key additions to the standard:
- **LE Isochronous Channels (ISO)** — A new controller-level transport for time-synchronized, low-latency data streams, supporting both connected (CIS) and connectionless (BIS) modes.
- **LC3 Codec** — The Low Complexity Communication Codec, which provides better audio quality at lower bitrates compared to SBC.
- **Generic Audio Framework (GAF)** — A layered set of profiles and services that standardize audio stream setup, volume control, media control, call control, and device coordination.
Bluetooth LE Audio supports two fundamental audio scenarios:
- **Unicast Audio** — Bidirectional or unidirectional audio between two connected devices over Connected Isochronous Streams (CIS). Typical use cases: TWS earbuds, hearing aids, headsets, telephony.
- **Broadcast Audio (Auracast™)** — Unidirectional audio from one broadcaster to any number of receivers over Broadcast Isochronous Streams (BIS). Typical use cases: public venue audio, accessibility assistive listening, group TV listening.
Specification Overview
----------------------
The Bluetooth LE Audio specification is organized into three tiers:
1. **Transport** — The underlying Bluetooth LE transport: LE Isochronous Channels (CIS/BIS) carry the audio data, the ACL connection carries GATT/ATT profile control, and periodic advertising carries broadcast announcements. Only the isochronous channels are new to LE Audio (detailed below).
2. **Generic Audio Framework (GAF)** — The core specification suite, organized into four functional layers: stream control, content control, rendering/capture control, and transition/coordination control.
3. **Use-Case Specific Profiles** — Higher-level profiles (HAP, TMAP, GMAP, PBP) that select and configure specific GAF components for a target use case.
.. figure:: ../../../_static/ble/ble-audio-gaf-en.png
:align: center
:width: 90%
:alt: Bluetooth LE Audio specification stack
The Bluetooth LE Audio stack: use-case profiles build on the Generic Audio Framework (GAF). Profile control rides GATT/ATT over the ACL connection, audio data rides the LE isochronous channels (CIS/BIS), and broadcast announcements ride periodic advertising.
Each GAF layer depends only on the layers below it. Use-case profiles select a subset of GAF layers and add role-specific constraints on top. The sections below describe each component in detail.
LE Isochronous Channels
-----------------------
LE Isochronous Channels are a feature of the Bluetooth controller, defined in the Bluetooth Core Specification. They provide the time-synchronized, low-latency data transport that Bluetooth LE Audio relies on.
.. list-table::
:header-rows: 1
:widths: 15 15 70
* - Type
- Abbreviation
- Description
* - Connected Isochronous Stream
- CIS
- Bidirectional isochronous link between two devices; requires a prior ACL connection. Multiple CIS instances can be grouped into a Connected Isochronous Group (CIG) for synchronized playback (e.g., left and right earbuds).
* - Broadcast Isochronous Stream
- BIS
- Unidirectional isochronous stream from a Broadcaster to any number of Synchronized Receivers, without a prior connection. Multiple BIS instances belong to a Broadcast Isochronous Group (BIG).
ESP-IDF provides direct access to CIS and BIS via the :doc:`ESP-BLE-ISO API <../../api-reference/bluetooth/esp-ble-iso>`. When using Bluetooth LE Audio profiles (BAP and above), the ISO layer is managed automatically by the profile stack.
Generic Audio Framework
-----------------------
The Generic Audio Framework (GAF) is the core of Bluetooth LE Audio. It defines four functional layers, described below from the bottom up.
Stream Control Layer
^^^^^^^^^^^^^^^^^^^^
The stream control layer is responsible for discovering audio capabilities, setting up audio streams (codec configuration and QoS), and managing the lifecycle of CIS and BIS connections.
**Basic Audio Profile (BAP)**
BAP is the foundational profile for all Bluetooth LE Audio streaming. It defines the following roles:
- **Unicast Client** — Discovers ASEs on a remote Unicast Server, initiates codec configuration, QoS negotiation, and stream control (enable, connect, start, disable, release).
- **Unicast Server** — Exposes audio endpoints (ASEs) via ASCS and responds to client-initiated stream control procedures.
- **Broadcast Source** — Creates a BIG, configures BIS streams, and sends audio data.
- **Broadcast Sink** — Scans for and synchronizes to a Broadcast Source, receives BIS audio data.
- **Broadcast Assistant** — Scans for Broadcast Sources on behalf of a low-power Scan Delegator and writes the results to BASS on the delegator.
- **Scan Delegator** — Exposes BASS and delegates BIS scanning to a Broadcast Assistant.
BAP depends on three GATT services:
.. list-table::
:header-rows: 1
:widths: 35 65
* - Service
- Role
* - Published Audio Capabilities Service (PACS)
- Exposes the device's supported codecs, codec configurations, and available audio contexts. Present on both Unicast Server and Broadcast Sink.
* - Audio Stream Control Service (ASCS)
- Exposes one or more Audio Stream Endpoints (ASEs), each representing a sink or source data path. Present on the Unicast Server only.
* - Broadcast Audio Scan Service (BASS)
- Used by the Scan Delegator to expose receive state; written by the Broadcast Assistant with Broadcast Source information. Present on the Scan Delegator only.
Content Control Layer
^^^^^^^^^^^^^^^^^^^^^
The content control layer provides standardized control over the media content and telephony activity that is being rendered by the audio stream.
**Media Control Profile (MCP) and Media Control Service (MCS)**
MCP defines a **Media Control Server** (which exposes a media player via MCS) and a **Media Control Client** (which discovers and controls the player). MCS exposes media state (playing/paused/stopped), playback position, track metadata, and control point operations (play, pause, next track, seek, etc.).
MCS comes in two forms:
- **MCS** — Per-player instance, for devices with multiple concurrent media players.
- **Generic MCS (GMCS)** — A single mandatory instance that provides access to the currently active player, used by clients that do not need per-player granularity.
MCP can optionally depend on the **Object Transfer Profile (OTP)** and **Object Transfer Service (OTS)** for transferring media objects (track names, icons, object metadata) when the device supports it.
**Call Control Profile (CCP) and Telephone Bearer Service (TBS)**
CCP defines a **Call Control Server** (which exposes one or more telephone bearers via TBS) and a **Call Control Client** (which discovers and controls calls). TBS exposes call state, call URI schemes, incoming/outgoing call control, signal strength, and provider name.
TBS comes in two forms:
- **TBS** — Per-bearer instance for devices with multiple telephony bearers (e.g., separate SIM cards or VoIP applications).
- **Generic TBS (GTBS)** — A single mandatory instance that provides a unified view of all bearers.
Rendering and Capture Control Layer
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The rendering and capture control layer provides standardized control over the audio output level and audio input gain of a device, independent of the content being played.
**Volume Control Profile (VCP)**
VCP defines a **Volume Renderer** (which exposes volume state and accepts remote control) and a **Volume Controller** (which discovers and controls the renderer). VCP depends on the following GATT services:
.. list-table::
:header-rows: 1
:widths: 35 15 50
* - Service
- Required
- Description
* - Volume Control Service (VCS)
- Mandatory
- Exposes volume setting (0–255), mute state, and a volume control point for absolute or relative volume changes.
* - Volume Offset Control Service (VOCS)
- Optional
- Allows per-output volume offset adjustment (e.g., different offsets for left and right channel outputs). A VCS may include one or more VOCS instances.
* - Audio Input Control Service (AICS)
- Optional
- Allows control of audio input gain and mute state for one audio input (e.g., microphone). A VCS may include one or more AICS instances.
**Microphone Control Profile (MICP)**
MICP defines a **Microphone Device** (which exposes microphone state) and a **Microphone Controller** (which discovers and mutes/unmutes it). MICP depends on the following GATT services:
.. list-table::
:header-rows: 1
:widths: 35 15 50
* - Service
- Required
- Description
* - Microphone Control Service (MICS)
- Mandatory
- Exposes the microphone mute state and a mute control point. Present on the Microphone Device.
* - Audio Input Control Service (AICS)
- Optional
- Allows control of audio input gain for specific inputs. A MICS may include one or more AICS instances.
.. note::
AICS is a shared service: it can be included by both VCS (as part of VCP) and MICS (as part of MICP), each as independent instances with separate handles.
Transition and Coordination Control Layer
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The transition and coordination control layer is the top of the GAF. It coordinates audio procedures across multiple devices acting as a group (e.g., a pair of TWS earbuds, or a room of speakers).
**Common Audio Profile (CAP)**
CAP is the top-level profile that defines how a single initiating device can coordinate audio operations (stream setup, volume control, microphone control) across one or more target devices. It defines three roles:
- **CAP Acceptor** — A device that accepts audio streams and volume/microphone control from a CAP Initiator or Commander. A CAP Acceptor shall support BAP Unicast Server or BAP Broadcast Sink (or both), and VCP Volume Renderer. MICP Microphone Device is optional.
- **CAP Initiator** — A device that discovers CAP Acceptors and initiates unicast or broadcast audio procedures using BAP, VCP, and MICP on one or more acceptors simultaneously.
- **CAP Commander** — A device that issues coordinated volume and microphone control commands to one or more CAP Acceptors without managing audio streams directly.
CAP depends on the **Common Audio Service (CAS)**, a mandatory GATT service on every CAP Acceptor. CAS is used for coordinated set member announcement and provides a stable discovery anchor for the CAP Initiator/Commander.
CAP also uses **CSIP** (described below) to identify and address the members of a coordinated set as a group.
**Coordinated Set Identification Profile (CSIP)**
CSIP defines how a group of devices (a "coordinated set") can be discovered and identified as belonging together. A common example is a left/right earbud pair: each earbud is a **CSIP Set Member** and a phone or source device is a **CSIP Set Coordinator**.
CSIP depends on the **Coordinated Set Identification Service (CSIS)**, which exposes:
- A **Set Identity Resolving Key (SIRK)** — Used by the Set Coordinator to match devices belonging to the same set, even across re-advertisements.
- **Set Size** — The number of members in the set.
- **Member Rank** — The rank of this device within the set (used for ordered operations).
Use-Case Specific Profiles
---------------------------
Use-case specific profiles sit on top of the GAF. Each profile selects a specific subset of GAF profiles, defines role-specific configuration constraints (e.g., codec parameters, QoS settings), and may add its own small GATT service for role advertisement.
**Hearing Access Profile (HAP)**
HAP targets hearing aid devices. It adds the concept of **hearing aid presets**: named audio configurations (e.g., "Outdoor", "Restaurant") that the user can select. HAP defines:
- **Hearing Aid** — Implements all GAF roles needed for audio reception, volume control, and (for binaural hearing aid pairs) coordinated set membership.
- **Hearing Aid Unicast Client** — Discovers hearing aids, controls presets, and manages unicast audio streams.
HAP depends on the **Hearing Access Service (HAS)** for preset read/write operations and on BAP, PACS, VCP, MICP, and CSIP from the GAF.
**Telephony and Media Audio Profile (TMAP)**
TMAP defines interoperability configurations for telephony and media use cases. It defines six roles:
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - Role
- Abbreviation
- Description
* - Call Gateway
- CG
- Controls calls on a remote CT using CCP/TBS. Sends and receives bidirectional audio over CIS.
* - Call Terminal
- CT
- Exposes calls via TBS; receives and sends bidirectional audio over CIS.
* - Unicast Media Sender
- UMS
- Sends unidirectional media audio to one or more UMRs over CIS. Acts as BAP Unicast Client and MCP server.
* - Unicast Media Receiver
- UMR
- Receives media audio from a UMS over CIS. Acts as BAP Unicast Server and VCP Volume Renderer.
* - Broadcast Media Sender
- BMS
- Sends media audio to any number of BMRs over BIS. Acts as BAP Broadcast Source.
* - Broadcast Media Receiver
- BMR
- Receives media audio from a BMS over BIS. Acts as BAP Broadcast Sink.
TMAP advertises its roles via the **Telephony and Media Audio Service (TMAS)**, a small GATT service containing a single TMAP Role characteristic. This allows a remote device to discover which TMAP roles the local device supports before establishing a connection. TMAP itself does not define new audio transport mechanisms; it delegates entirely to BAP (for stream setup), VCP (for volume), MCP/MCS (for media control in UMS/UMR), and CCP/TBS (for call control in CG/CT).
**Gaming Audio Profile (GMAP)**
GMAP targets gaming audio products with parameters tuned for lower transport latency and fewer retransmissions. It defines four roles:
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - Role
- Abbreviation
- Description
* - Unicast Game Gateway
- UGG
- Sends game audio to UGTs and optionally receives voice audio back. Acts as BAP Unicast Client.
* - Unicast Game Terminal
- UGT
- Receives game audio from a UGG and optionally sends voice audio back. Acts as BAP Unicast Server.
* - Broadcast Game Sender
- BGS
- Sends game audio over BIS. Acts as BAP Broadcast Source.
* - Broadcast Game Receiver
- BGR
- Receives game audio over BIS. Acts as BAP Broadcast Sink.
GMAP advertises roles via the **Gaming Audio Service (GMAS)** and depends on BAP for stream setup and VCP for volume control.
**Public Broadcast Profile (PBP)**
PBP standardizes the metadata format used by a public broadcast source so that any compatible receiver can discover and synchronize to it without prior pairing. It defines:
- **Public Broadcast Source** — Advertises Auracast™ audio streams with standardized extended advertising data including the Broadcast Audio Announcement and Public Broadcast Announcement. Delegates to BAP Broadcast Source for BIS setup.
- **Public Broadcast Sink** — Scans for PBP sources, reads the announcement metadata to determine audio quality and content, and synchronizes to the BIG. Delegates to BAP Broadcast Sink.
PBP depends entirely on BAP for the underlying broadcast transport; it does not define a new GATT service.
Profile and Service Dependencies
--------------------------------
The diagram below expands each abbreviation to its full name and shows the layered dependency hierarchy — solid arrows are a depends-on relationship, dotted arrows an optional included sub-service. The tables that follow give the exact per-profile dependencies.
.. mermaid::
%%{init: {'flowchart': {'nodeSpacing': 35, 'rankSpacing': 65}}}%%
flowchart LR
HAP["HAP<br/>(Hearing Access Profile)"]
TMAP["TMAP<br/>(Telephony and Media Audio Profile)"]
GMAP["GMAP<br/>(Gaming Audio Profile)"]
PBP["PBP<br/>(Public Broadcast Profile)"]
CAP["CAP<br/>(Common Audio Profile)"]
VCP["VCP<br/>(Volume Control Profile)"]
MICP["MICP<br/>(Microphone Control Profile)"]
CSIP["CSIP<br/>(Coordinated Set Identification Profile)"]
MCP["MCP<br/>(Media Control Profile)"]
CCP["CCP<br/>(Call Control Profile)"]
BAP["BAP<br/>(Basic Audio Profile)"]
HAS["HAS<br/>(Hearing Access Service)"]
TMAS["TMAS<br/>(Telephony and Media Audio Service)"]
GMAS["GMAS<br/>(Gaming Audio Service)"]
CAS["CAS<br/>(Common Audio Service)"]
PACS["PACS<br/>(Published Audio Capabilities Service)"]
ASCS["ASCS<br/>(Audio Stream Control Service)"]
BASS["BASS<br/>(Broadcast Audio Scan Service)"]
CSIS["CSIS<br/>(Coordinated Set Identification Service)"]
VCS["VCS<br/>(Volume Control Service)"]
MICS["MICS<br/>(Microphone Control Service)"]
MCS["MCS / GMCS<br/>(Media Control Service)"]
TBS["TBS / GTBS<br/>(Telephone Bearer Service)"]
VOCS["VOCS<br/>(Volume Offset Control Service)"]
AICS["AICS<br/>(Audio Input Control Service)"]
OTS["OTS<br/>(Object Transfer Service)"]
HAP --> CAP
TMAP --> CAP & MCP & CCP
GMAP --> CAP
PBP --> BAP
CAP --> BAP & VCP & MICP & CSIP
CAP --> CAS
HAP --> HAS
TMAP --> TMAS
GMAP --> GMAS
BAP --> PACS & ASCS & BASS
VCP --> VCS
MICP --> MICS
CSIP --> CSIS
MCP --> MCS
CCP --> TBS
VCS -.-> VOCS & AICS
MICS -.-> AICS
MCS -.-> OTS
CAP ~~~ MCP
CAP ~~~ CCP
classDef uc fill:#dbe8ff,stroke:#5b8def,color:#173;
classDef coord fill:#ffe6c7,stroke:#e0922f;
classDef ctrl fill:#e3f6da,stroke:#5aa84f;
classDef svc fill:#efe3ff,stroke:#9a6fd6;
classDef opt fill:#f0f0f0,stroke:#9e9e9e,color:#555;
class HAP,TMAP,GMAP,PBP uc;
class CAP,BAP coord;
class VCP,MICP,CSIP,MCP,CCP ctrl;
class HAS,TMAS,GMAS,CAS,PACS,ASCS,BASS,CSIS,VCS,MICS,MCS,TBS svc;
class VOCS,AICS,OTS opt;
Profile-to-Service Dependencies
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:header-rows: 1
:widths: 22 20 58
* - Profile
- Depends on (Services)
- Notes
* - BAP
- PACS, ASCS, BASS
- PACS on Unicast Server and Broadcast Sink; ASCS on Unicast Server only; BASS on Scan Delegator only.
* - VCP
- VCS (mandatory), VOCS (optional), AICS (optional)
- VOCS and AICS are sub-included services within VCS; each may have multiple instances.
* - MICP
- MICS (mandatory), AICS (optional)
- AICS is a sub-included service within MICS.
* - CAP
- CAS (mandatory)
- CAS must be present on every CAP Acceptor. CAP also uses BAP, VCP, MICP, and CSIP procedures.
* - CSIP
- CSIS (mandatory)
- CSIS on the Set Member device.
* - MCP
- MCS / GMCS (mandatory), OTS (optional)
- GMCS is the single mandatory generic instance; per-player MCS instances are optional. OTS is used when media objects are available.
* - CCP
- TBS / GTBS (mandatory)
- GTBS is the single mandatory generic instance; per-bearer TBS instances are optional.
* - HAP
- HAS (mandatory)
- HAS for preset control. HAP also mandates BAP, PACS, VCP, MICP, and CSIP (for binaural sets).
* - TMAP
- TMAS (mandatory)
- TMAS contains only the TMAP Role characteristic. TMAP delegates stream and control operations to BAP, VCP, MCP, and CCP.
* - GMAP
- GMAS (mandatory)
- GMAS contains only the GMAP Role characteristic. GMAP delegates to BAP and VCP.
* - PBP
- None (no dedicated service)
- PBP uses BAP Broadcast Source/Sink and standardized extended advertising metadata only.
Profile-to-Profile Dependencies
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:header-rows: 1
:widths: 22 22 56
* - Profile
- Depends on (Profiles)
- Notes
* - CAP Initiator / Commander
- BAP, VCP, MICP, CSIP
- Uses BAP for stream setup, VCP and MICP for rendering/capture control, CSIP to address a coordinated set of Acceptors.
* - HAP
- BAP, VCP, MICP, CSIP, CAP
- CAP Acceptor role is mandatory on Hearing Aid devices. CSIP is required for binaural hearing aid pairs.
* - TMAP CG / CT
- BAP (unicast), VCP, CCP
- CG also acts as MCP server (media proxy) in some implementations.
* - TMAP UMS / UMR
- BAP (unicast), VCP, MCP
- —
* - TMAP BMS / BMR
- BAP (broadcast), VCP
- —
* - GMAP UGG / UGT
- BAP (unicast), VCP
- —
* - GMAP BGS / BGR
- BAP (broadcast), VCP
- —
* - PBP Source / Sink
- BAP (broadcast)
- —
Typical Use-Case Profiles
^^^^^^^^^^^^^^^^^^^^^^^^^^
The table below maps common product types to the profiles they require.
.. list-table::
:header-rows: 1
:widths: 30 70
* - Product Type
- Required Profiles / Roles
* - TWS Earbuds (receiver side)
- CAP Acceptor, BAP Unicast Server, VCP Volume Renderer, CSIP Set Member, MICP Microphone Device (if mic present)
* - Phone / Audio Source
- CAP Initiator, BAP Unicast Client, VCP Volume Controller, CSIP Set Coordinator
* - Hearing Aid
- HAP Hearing Aid, CAP Acceptor, BAP Unicast Server, VCP Volume Renderer, CSIP Set Member (for binaural pair)
* - TV / Broadcast Source
- BAP Broadcast Source, PBP Public Broadcast Source (for Auracast™)
* - Hearing Loop Receiver
- BAP Broadcast Sink, PBP Public Broadcast Sink
* - Telephony Headset
- TMAP CT + UMR (or CG + UMS for gateway side), VCP, CCP
* - Gaming Headset
- GMAP UGT (receiver), BAP Unicast Server, VCP Volume Renderer
* - Media Sender (audio bar)
- TMAP UMS (or BMS for broadcast), BAP, MCP/MCS server, VCP