Merge branch 'idf/ble_audio_fixes' into 'master'

fix(ble_audio): Miscellaneous fixes for ISO & LE Audio (stage 2)

See merge request espressif/esp-idf!51158
This commit is contained in:
Island
2026-08-05 10:47:55 +08:00
113 changed files with 4547 additions and 2104 deletions
@@ -64,7 +64,7 @@ For `esp32s31`, replace the chip overlay accordingly.
6. `ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC` clears `pa_syncing`, cancels discovery, stores `sync_handle`, and calls `esp_ble_audio_bap_broadcast_sink_create()`.
7. `base_recv_cb` extracts the subgroup count and BIS index bitfield (masked by `bis_index_mask`); when no Broadcast Assistant is connected, `requested_bis_sync` defaults to `ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF`.
8. `syncable_cb` AND-masks the BASE bitfield with the requested mask, copies `TARGET_BROADCAST_CODE` if the BIG is encrypted (unless BASS already supplied one), and calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen mask and `streams_p`.
9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` deletes the broadcast sink. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and restarts the scanner.
9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` clears `stream_started`; the sink itself is deleted from `broadcast_sink_stopped_cb` (the `stopped` sink callback), which runs once BASS has cleared `bis_sync` — deleting from `stream_stopped_cb` would race `rem_src` while `bis_sync` is still non-zero. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and restarts the scanner.
## Expected Log
@@ -92,6 +92,7 @@ On teardown / sync loss:
```
I (xxx) BAP_BSNK: [SNK #0] Stream stopped, reason 0x... (.../...)
I (xxx) BAP_BSNK: Broadcast sink stopped, reason 0x...
I (xxx) BAP_BSNK: PA sync lost: sync_handle ... reason 0x...
I (xxx) BAP_BSNK: PA sync terminated
```
@@ -105,4 +106,4 @@ Run [broadcast_source](../broadcast_source/) on a second board. Expected interac
3. Source's BIGInfo advertises the BIG as encrypted (broadcast code `"1234"`); sink reports `BIG encrypted`.
4. Source starts the BIG and the two BIS streams (`FRONT_LEFT`, `FRONT_RIGHT`); sink calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen BIS bitfield and the matching broadcast code.
5. Source's TX scheduler keeps pushing SDUs at `preset_active.qos.interval`; sink stream `recv` callbacks deliver the data and update RX metrics.
6. Stopping the source (or losing PA sync) triggers `stream_stopped_cb` on the sink, which deletes the broadcast sink and resumes scanning.
6. Stopping the source (or losing PA sync) tears down the BIG; `broadcast_sink_stopped_cb` then deletes the sink (after BASS clears `bis_sync`) and scanning resumes.
@@ -209,23 +209,14 @@ static int bis_sync_req_cb(esp_ble_conn_t *conn,
stream_started ? "streaming" : "not streaming");
if (stream_started && requested_bis_sync == 0) {
/* The stream stopped callback will be called as part of this, and
* we do not need to wait for any events from the controller. Thus,
* when this returns, the `stream_started` is back to false.
/* stop() tears down the BIG; broadcast_sink_stopped_cb deletes the
* sink after BASS bis_sync has been cleared.
*/
err = esp_ble_audio_bap_broadcast_sink_stop(broadcast_sink);
if (err) {
ESP_LOGE(TAG, "Failed to stop broadcast sink, err %d", err);
return -EIO;
}
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
if (err) {
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
return -EIO;
}
broadcast_sink = NULL;
}
return 0;
@@ -319,9 +310,36 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
}
}
static void broadcast_sink_stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
uint8_t reason)
{
esp_err_t err;
ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason);
stream_started = false;
/* Called from big_stopped after update_recv_state_big_cleared(), so
* BASS bis_sync is already 0 and rem_src inside delete can succeed.
* Do not delete from stream_ops.stopped — that runs before bis_sync clear.
*/
if (broadcast_sink == NULL) {
return;
}
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
if (err) {
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
return;
}
broadcast_sink = NULL;
}
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
.base_recv = base_recv_cb,
.syncable = syncable_cb,
.stopped = broadcast_sink_stopped_cb,
};
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
@@ -352,21 +370,14 @@ static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
{
esp_err_t err;
ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x (%u/%u)",
stream_index(stream), reason, stream_count_stopped, stream_count);
if (++stream_count_stopped == stream_count) {
stream_started = false;
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
if (err) {
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
return;
}
broadcast_sink = NULL;
/* Sink delete is deferred to broadcast_sink_stopped_cb (after BASS
* bis_sync clear). Deleting here races rem_src and leaves WRNs.
*/
}
}
@@ -61,8 +61,8 @@ For `esp32s31`, replace the chip overlay accordingly.
3. On a connectable match `ext_scan_recv` calls `ext_scan_stop` and then `conn_create` (NimBLE: `ble_gap_connect`; Bluedroid: `esp_ble_gattc_aux_open`); the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `pairing_start` (NimBLE: `ble_gap_security_initiate`; Bluedroid: `esp_ble_set_encryption`).
4. After the security-change event the client triggers `exchange_mtu`. On NimBLE this issues `ble_gattc_exchange_mtu`; on Bluedroid the GATTC adapter auto-configures MTU inside `BTA_GATTC_Enh_Open`, so `exchange_mtu` drives `esp_ble_audio_gattc_disc_start` directly. `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start` (the retry is idempotent — `-EALREADY` is treated as success), and once both MTU and discovery are reported `discover_sinks` runs.
5. `discover_cb` chains sink discovery into `discover_sources`; `endpoint_cb` records each EP into the next free `sinks[]`/`sources[]` slot, and source completion kicks `configure_stream`, which configures every EP with `unicast_preset.codec_cfg`.
6. After the last `config_cb`, `create_group` builds `pair_params` (sinks first, then sources) with `ESP_BLE_ISO_PACKING_SEQUENTIAL` and calls `esp_ble_audio_bap_unicast_group_create`, then `set_stream_qos`.
7. When `is_all_stream_qos_set` returns true `enable_stream` walks every stream; on the final `enable_cb`, `connect_stream` ISO-connects each pair (sink first if available); when all are up `start_stream` starts only source streams. Sink streams are started by the server, and `stream_started_cb` calls `stream_tx_register` to begin TX.
6. `config_cb` configures each EP per Config CP ACK until all are done (`all_config_acked`); `create_group` (building `pair_params`, sinks first then sources, with `ESP_BLE_ISO_PACKING_SEQUENTIAL`) and `set_stream_qos` then run from `try_create_group_and_qos`, which waits for every EP to reach codec-configured (`stream_configured_cb` / `ops.configured`) — the CP ACK can arrive before the ASE state change, so driving QoS off the ACK alone races the last EP still in idle.
7. `enable_stream` runs once every EP reports qos-configured (`stream_qos_set_cb` / `ops.qos_set`), not off the QoS CP ACK; `connect_stream` ISO-connects each pair (sink first) once every EP reports enabling (`stream_enabled_cb` / `ops.enabled`), not off the Enable CP ACK. (The ACK callbacks `qos_cb` / `enable_cb` only handle rejects — an ACK can outrun the ASE state change.) When all are up `start_stream` starts only source streams. Sink streams are started by the server, and `stream_started_cb` calls `stream_tx_register` to begin TX.
## Expected Log
@@ -59,6 +59,12 @@ static size_t configured_sink_stream_count;
static size_t configured_source_stream_count;
#define configured_stream_count (configured_sink_stream_count + configured_source_stream_count)
/* Number of streams that have entered codec-configured (ops.configured).
* CP config ACK can arrive while the EP is still idle; QoS must wait for this.
*/
static size_t codec_configured_count;
static bool all_config_acked;
static struct stream_pair_state {
esp_ble_audio_bap_stream_t *sink_stream;
esp_ble_audio_bap_stream_t *source_stream;
@@ -147,6 +153,8 @@ static void reset_stream_state(void)
{
configured_sink_stream_count = 0;
configured_source_stream_count = 0;
codec_configured_count = 0;
all_config_acked = false;
reset_stream_pair_state();
@@ -388,6 +396,51 @@ static int set_stream_qos(void)
return 0;
}
static void try_create_group_and_qos(void)
{
uint8_t stream_count = 0;
int err;
/* Wait until every successful Config CP has a matching ASE state
* notification (ops.configured → EP codec-configured). Calling
* qos from config_cb alone races the last stream still in idle.
*/
if (!all_config_acked || unicast_group != NULL) {
return;
}
if (configured_stream_count == 0) {
ESP_LOGW(TAG, "No streams were configured");
return;
}
if (codec_configured_count < configured_stream_count) {
return;
}
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
if (sinks[i].configured == ASCS_RSP_SUCCESS) {
streams[stream_count++] = &sinks[i].stream;
}
}
for (size_t i = 0; i < ARRAY_SIZE(sources); i++) {
if (sources[i].configured == ASCS_RSP_SUCCESS) {
streams[stream_count++] = &sources[i].stream;
}
}
err = create_group();
if (err) {
return;
}
err = set_stream_qos();
if (err) {
return;
}
}
static void stream_qos_set(esp_ble_audio_bap_stream_t *stream, bool success)
{
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
@@ -682,18 +735,35 @@ static void stream_configured_cb(esp_ble_audio_bap_stream_t *stream,
ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:",
stream_dir_str(stream), stream_index(stream));
example_print_qos_pref(TAG, pref);
codec_configured_count++;
try_create_group_and_qos();
}
static void stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
{
/* QoS set is also reported by qos_cb; skip the duplicate log here. */
(void)stream;
/* ASE entered qos-configured — safe to enable. */
stream_qos_set(stream, true);
if (is_all_stream_qos_set()) {
enable_stream();
}
}
static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
{
/* Enabled is also reported by enable_cb; skip the duplicate log here. */
(void)stream;
bool ret;
/* ASE entered enabling — safe to connect after all enables complete. */
stream_enabled(stream, true);
ret = enable_stream();
if (ret == false) {
return;
}
connect_stream();
}
static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream)
@@ -843,9 +913,7 @@ static void config_cb(esp_ble_audio_bap_stream_t *stream,
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
esp_ble_audio_bap_ascs_reason_t reason)
{
uint8_t stream_count;
bool ret;
int err;
log_rsp("Config", stream, rsp_code, reason);
@@ -856,34 +924,9 @@ static void config_cb(esp_ble_audio_bap_stream_t *stream,
return;
}
if (configured_stream_count == 0) {
ESP_LOGW(TAG, "No streams were configured");
return;
}
all_config_acked = true;
stream_count = 0;
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
if (sinks[i].configured == ASCS_RSP_SUCCESS) {
streams[stream_count++] = &sinks[i].stream;
}
}
for (size_t i = 0; i < ARRAY_SIZE(sources); i++) {
if (sources[i].configured == ASCS_RSP_SUCCESS) {
streams[stream_count++] = &sources[i].stream;
}
}
err = create_group();
if (err) {
return;
}
err = set_stream_qos();
if (err) {
return;
}
try_create_group_and_qos();
}
static void qos_cb(esp_ble_audio_bap_stream_t *stream,
@@ -892,10 +935,14 @@ static void qos_cb(esp_ble_audio_bap_stream_t *stream,
{
log_rsp("QoS", stream, rsp_code, reason);
stream_qos_set(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS);
if (is_all_stream_qos_set()) {
enable_stream();
/* Mark failure from CP reject. Success waits for ops.qos_set (ASE
* qos-configured) — enabling on CP ACK alone races codec-configured.
*/
if (rsp_code != ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS) {
stream_qos_set(stream, false);
if (is_all_stream_qos_set()) {
enable_stream();
}
}
}
@@ -903,18 +950,19 @@ static void enable_cb(esp_ble_audio_bap_stream_t *stream,
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
esp_ble_audio_bap_ascs_reason_t reason)
{
bool ret;
log_rsp("Enable", stream, rsp_code, reason);
stream_enabled(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS);
/* Same as QoS: connect needs ENABLING from ASE state ntf (ops.enabled). */
if (rsp_code != ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS) {
bool ret;
ret = enable_stream();
if (ret == false) {
return;
stream_enabled(stream, false);
ret = enable_stream();
if (ret) {
connect_stream();
}
}
connect_stream();
}
static void start_cb(esp_ble_audio_bap_stream_t *stream,
@@ -101,6 +101,12 @@ int stream_tx_register(esp_ble_audio_bap_stream_t *stream)
return -EINVAL;
}
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
if (tx_streams[i].stream == stream) {
return 0; /* already armed */
}
}
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
if (tx_streams[i].stream == NULL) {
if (stream->qos == NULL || stream->qos->sdu == 0) {
@@ -128,6 +128,7 @@ Source (SRC) Assistant (ASS) Acceptor (ACC)
| `FLAG_BROADCAST_CODE_RECEIVED` | `broadcast_code_cb` (BASS Set Broadcast Code); self-scan local code | `broadcast_sink_reset` |
| `FLAG_BROADCAST_SYNC_REQUESTED` | `bis_sync_req_cb` (bitmap ≠ 0); self-scan PA match | `bis_sync_req_cb` (bitmap = 0); `broadcast_sink_reset` |
| `FLAG_BROADCAST_RESYNC_PENDING` | `bis_sync_req_cb` before `_stop` (bitmap change while streaming) | `stream_stopped_cb` after driving the re-sync; `_stop` failure; `broadcast_sink_reset` |
| `FLAG_BROADCAST_STOP_PENDING` | `broadcast_stream_stopped_cb` (last BIS stops) | `stopped_cb`; `broadcast_sink_reset` |
| `FLAG_BROADCAST_SYNCING` | `check_sync_broadcast` after `_sync` returns OK | `stream_started_cb`; `stream_stopped_cb` |
| `FLAG_BROADCAST_SYNCED` | `stream_started_cb` | `stream_stopped_cb` |
@@ -161,9 +162,9 @@ BASE_RECEIVED && BROADCAST_SYNCABLE
+-----+-----+ |
| | BIG drops while PA gone
| v
+---------> stream_stopped_cb + !PA_SYNCED
+--> stream_stopped_cb (set STOP_PENDING, !PA_SYNCED)
|
| _delete + broadcast_sink_reset
| stopped_cb: _delete + broadcast_sink_reset
v
[end]
```
@@ -171,8 +172,8 @@ BASE_RECEIVED && BROADCAST_SYNCABLE
Key invariants:
- **PA loss does NOT tear down a running BIS.** Per BASS § 3.2.1.6 / § 3.2.1.9, `PA_Sync_State` and `BIS_Sync_State` are independent. While BIS is streaming/syncing, `broadcast_pa_lost` only notifies the assistant (`PA_Sync_State = 0x00`) and clears PA-only local state (`sync_handle`, `FLAG_PA_SYNCED`); the BIG keeps running and audio continues to flow.
- **PA loss with BIS idle tears down the sink.** The sink is bound to the now-dead sync handle and its cached BASE / BIGInfo are stale. `broadcast_pa_lost` calls `_delete` and clears `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The assistant's subscription (`requested_bis_sync`, `FLAG_BROADCAST_SYNC_REQUESTED`, `FLAG_BROADCAST_CODE_RECEIVED`) is preserved so the next PA sync re-creates a fresh sink and resumes streaming.
- **Sink deletion happens in `stream_stopped_cb` when both PA and BIS are gone.** Triggers: assistant unsubscribes via `Modify Source bis_sync = 0`, or the broadcaster stops the BIG while PA is already gone.
- **PA loss with BIS idle tears down the sink.** The sink is bound to the now-dead sync handle and its cached BASE / BIGInfo are stale. `broadcast_pa_lost` calls `_delete` (unless `FLAG_BROADCAST_STOP_PENDING` is set, in which case `stopped_cb` deletes after `bis_sync` clear) and clears `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The assistant's subscription (`requested_bis_sync`, `FLAG_BROADCAST_SYNC_REQUESTED`, `FLAG_BROADCAST_CODE_RECEIVED`) is preserved so the next PA sync re-creates a fresh sink and resumes streaming.
- **Sink deletion happens in `stopped_cb` (the `stopped` sink callback) once BASS has cleared `bis_sync`, when PA is gone.** `broadcast_stream_stopped_cb` of the last BIS only sets `FLAG_BROADCAST_STOP_PENDING` (and re-`_sync`s on a pending bitmap change) — it does not delete, because deleting there races `rem_src` while `bis_sync` is still non-zero. Triggers: assistant unsubscribes via `Modify Source bis_sync = 0`, or the broadcaster stops the BIG while PA is already gone.
- `bis_sync_req_cb` going `X → 0` (Assistant pause) only issues `_stop`, never `_delete`. Going `X → Y` (BIS bitmap switch) likewise only `_stop`s; the next `check_sync_broadcast` (called from `stream_stopped_cb` when PA still synced) re-`_sync`s the same object.
- `pa_sync_term_req_cb` issues the HCI Periodic Advertising Terminate Sync but does **not** clear `broadcast_sink.sync_handle`. Cleanup runs from `BLE_GAP_EVENT_PERIODIC_SYNC_LOST` → `broadcast_pa_lost`. Resetting the handle early would make that gate miss.
@@ -181,17 +182,17 @@ Key invariants:
| Event | Action |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| **PA lost while BIS active** (broadcaster moved mid-stream) | Set `PA_Sync_State = 0x00`; clear PA-only state (`sync_handle`, `FLAG_PA_SYNCED`). Sink + BIS untouched; BIG keeps running. |
| **PA lost while BIS idle** (Assistant `PA_Sync = 0`, or PA dropped after Assistant paused BIS) | Set `PA_Sync_State = 0x00` (skipped if BASS already updated it in-place); clear PA-only state; `_delete` the sink and clear `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The next PA sync starts from a clean sink and lets the lib redeliver BASE / BIGInfo. |
| **PA lost while BIS idle** (Assistant `PA_Sync = 0`, or PA dropped after Assistant paused BIS) | Set `PA_Sync_State = 0x00` (skipped if BASS already updated it in-place); clear PA-only state; unless `FLAG_BROADCAST_STOP_PENDING` is set (BIG teardown in flight — `stopped_cb` deletes after `bis_sync` clear), `_delete` the sink and clear `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The next PA sync starts from a clean sink and lets the lib redeliver BASE / BIGInfo. |
| **Modify Source `bis_sync = 0`** (PA still synced) | `bis_sync_req_cb` clears `FLAG_BROADCAST_SYNC_REQUESTED` then `_stop`s the BIG. Sink retained. |
| **Modify Source bitmap change** (PA still synced, streaming) | Update `requested_bis_sync` + `FLAG_BROADCAST_SYNC_REQUESTED` + set `FLAG_BROADCAST_RESYNC_PENDING`, then `_stop`. `stream_stopped_cb` clears the flag and re-`_sync`s with the new bitmap. |
| **BIG drops while PA still synced** (e.g. broadcaster pause) | `stream_stopped_cb` clears SYNCED/SYNCING and exposes the loss via `BIS_Sync_State`. `FLAG_BROADCAST_RESYNC_PENDING` is not set, so no auto-retry — per BASS § 3.2.1.9 the assistant drives recovery via Modify Source. |
| **BIG drops after PA lost** (broadcaster turned off) | `stream_stopped_cb` of the last active stream sees `!PA_SYNCED` → `_delete` + `broadcast_sink_reset`. Multi-BIS: earlier callbacks just decrement `active_streams` so `_delete` is not called while the sink is still in use. |
| **BIG drops after PA lost** (broadcaster turned off) | `broadcast_stream_stopped_cb` of the last active stream sees `!PA_SYNCED` and sets `FLAG_BROADCAST_STOP_PENDING`; `stopped_cb` then runs `_delete` + `broadcast_sink_reset` once BASS has cleared `bis_sync`. Multi-BIS: earlier callbacks just decrement `active_streams` so the flag is not set while the sink is still in use. |
| **Assistant Remove Source** | Spec allows only when BIS not synced; lib handles, app sees no special event. |
Two recurring patterns that drive the above behavior:
- **Update local state before calling `_stop`/`_delete`.** The lib may fire `stream_stopped_cb` synchronously from within `_stop`, so the callback must see the post-stop state. Applies in `bis_sync_req_cb` (updates `requested_bis_sync` + flag before `_stop`) and `broadcast_pa_lost` (no longer calls `_stop`).
- **Sink lifetime is BIS-driven, not PA-driven.** Sink is created on first PA sync and deleted only when the BIG itself stops and PA is also gone. This matches BASS spec's independent PA/BIS state model.
- **Sink lifetime is BIS-driven, not PA-driven.** Sink is created on first PA sync and deleted (from `stopped_cb`, after BASS clears `bis_sync`) only when the BIG itself stops and PA is also gone. This matches BASS spec's independent PA/BIS state model.
### Multi-BIS (stereo) configuration
@@ -290,6 +291,7 @@ On PA sync loss:
```
I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x...
I (xxx) CAP_ACC: Broadcast sink stopped, reason 0x...
I (xxx) CAP_ACC: PA sync lost: sync_handle ... reason 0x...
I (xxx) CAP_ACC: Scanning for broadcast source...
```
@@ -29,6 +29,11 @@ enum broadcast_flag {
FLAG_BROADCAST_SYNCING,
FLAG_BROADCAST_SYNCED,
FLAG_BROADCAST_RESYNC_PENDING,
/* Set when the last BIS stream stops; cleared in stopped_cb.
* Bridges the window where EPs are IDLE but big_stopped has not yet cleared
* BASS bis_sync — pa_lost must not rem_src/delete during that window.
*/
FLAG_BROADCAST_STOP_PENDING,
FLAG_BASE_RECEIVED,
FLAG_PA_SYNCING,
FLAG_PA_SYNCED,
@@ -201,6 +206,7 @@ static void broadcast_sink_reset(void)
flag_clear(FLAG_BROADCAST_CODE_RECEIVED);
flag_clear(FLAG_BROADCAST_SYNC_REQUESTED);
flag_clear(FLAG_BROADCAST_RESYNC_PENDING);
flag_clear(FLAG_BROADCAST_STOP_PENDING);
flag_clear(FLAG_SCANNING);
#if CONFIG_EXAMPLE_SCAN_SELF
@@ -330,8 +336,6 @@ static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
{
esp_err_t err;
ESP_LOGI(TAG, "[SNK #%u] Stream stopped, reason 0x%02x",
broadcast_stream_idx(stream), reason);
@@ -349,21 +353,14 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint
flag_clear(FLAG_BROADCAST_SYNCING);
flag_clear(FLAG_BROADCAST_SYNCED);
/* Mark BIG teardown in progress until stopped_cb runs
* (after update_recv_state_big_cleared). Do not delete here — that
* races rem_src while bis_sync is still non-zero.
*/
flag_set(FLAG_BROADCAST_STOP_PENDING);
if (flag_test(FLAG_PA_SYNCED) == false) {
/* Both PA and BIS are gone — no path to recover BIGInfo, so the
* sink can no longer drive a new BIG sync. Delete it and clear all
* state.
*/
if (broadcast_sink.sink) {
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink);
if (err) {
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
return;
}
}
broadcast_sink_reset();
/* Both PA and BIS are gone — stopped_cb deletes. */
return;
}
@@ -378,6 +375,35 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint
}
}
static void stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
uint8_t reason)
{
esp_err_t err;
ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason);
flag_clear(FLAG_BROADCAST_STOP_PENDING);
/* Keep the sink when PA is still synced (assistant pause, BIS bitmap
* resync, or spontaneous BIG drop with PA alive).
*/
if (flag_test(FLAG_PA_SYNCED)) {
return;
}
if (broadcast_sink.sink == NULL) {
return;
}
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink);
if (err) {
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
return;
}
broadcast_sink_reset();
}
static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
const esp_ble_iso_recv_info_t *info,
const uint8_t *data, uint16_t len)
@@ -740,14 +766,15 @@ void broadcast_pa_lost(uint16_t sync_handle)
flag_clear(FLAG_PA_SYNCED);
flag_clear(FLAG_PA_SYNCING);
/* BIS still active → BIG keeps running per § 3.2.1.9, leave the sink;
* stream_stopped_cb will tear it down when BIS eventually stops.
/* BIS still active or BIG teardown pending → leave the sink;
* stopped_cb will tear it down when bis_sync is clear.
* Otherwise the sink is bound to a dead PA handle and its cached
* BASE / BIGInfo are stale: delete the sink and clear the PA-derived
* flags so the next PA sync creates a fresh sink and lets the lib
* redeliver BASE / BIGInfo.
*/
if (flag_test(FLAG_BROADCAST_SYNCING) || flag_test(FLAG_BROADCAST_SYNCED)) {
if (flag_test(FLAG_BROADCAST_SYNCING) || flag_test(FLAG_BROADCAST_SYNCED) ||
flag_test(FLAG_BROADCAST_STOP_PENDING)) {
return;
}
@@ -789,6 +816,7 @@ int cap_acceptor_broadcast_init(void)
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
.base_recv = base_recv_cb,
.syncable = syncable_cb,
.stopped = stopped_cb,
};
static esp_ble_audio_bap_stream_ops_t broadcast_stream_ops = {
.started = broadcast_stream_started_cb,
@@ -59,7 +59,7 @@ For `esp32s31`, replace the chip overlay accordingly.
5. On `PA_SYNC` success, cancel scanning, log the sync handle, and create the broadcast sink for the sync handle and broadcast ID.
6. The `base_recv` callback extracts the BIS index bitfield masked by the available stream count; the `syncable` callback then calls `esp_ble_audio_bap_broadcast_sink_sync` with the stream pointer array.
7. Per-stream `started` callback resets RX metrics; `recv` callback feeds each SDU into `example_audio_rx_metrics_on_recv` (tracking valid/error/lost/zero-length counts); `stopped` logs the reason.
8. On `PA_SYNC_LOST` matching the active sync handle, delete the broadcast sink and re-enter scanning.
8. On `PA_SYNC_LOST` matching the active sync handle, the sink is deleted — by `broadcast_sink_stopped_cb` once BASS clears `bis_sync` (deleting from `stream_stopped_cb` races `rem_src` while `bis_sync` is still set), or directly here if the sink never reached BIG sync — and scanning resumes.
## Expected Log
@@ -90,6 +90,7 @@ I (xxx) TMAP_BMR: [SNK #0] Stream started
Stop / sync loss:
```
I (xxx) TMAP_BMR: [SNK #0] Stream stopped, reason 0x..
I (xxx) TMAP_BMR: Broadcast sink stopped, reason 0x..
I (xxx) TMAP_BMR: PA sync lost: sync_handle .. reason 0x..
I (xxx) TMAP_BMR: PA sync .. lost with reason ..
```
@@ -21,6 +21,10 @@ static bool tmap_bms_found;
static esp_ble_audio_bap_broadcast_sink_t *broadcast_sink;
static uint32_t bcast_id;
/* True after a BIS stream stops until broadcast_sink_stopped_cb runs —
* pa_lost must not delete in that window (EPs idle, bis_sync not yet cleared).
*/
static bool stop_pending;
static esp_ble_audio_bap_stream_t streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT];
static esp_ble_audio_bap_stream_t *streams_p[ARRAY_SIZE(streams)];
@@ -72,6 +76,11 @@ static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason
{
ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x",
stream_index(stream), reason);
/* Sink delete is deferred to broadcast_sink_stopped_cb (after BASS
* bis_sync clear). Deleting while EPs are idle but bis_sync is still
* set races rem_src and leaves WRNs.
*/
stop_pending = true;
}
static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
@@ -129,9 +138,35 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
}
}
static void broadcast_sink_stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
uint8_t reason)
{
esp_err_t err;
ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason);
stop_pending = false;
/* Called from big_stopped after update_recv_state_big_cleared(), so
* BASS bis_sync is already 0 and rem_src inside delete can succeed.
*/
if (broadcast_sink == NULL) {
return;
}
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
if (err) {
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
return;
}
broadcast_sink = NULL;
}
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
.base_recv = base_recv_cb,
.syncable = syncable_cb,
.stopped = broadcast_sink_stopped_cb,
};
static esp_ble_audio_bap_scan_delegator_cb_t scan_delegator_cbs;
@@ -254,6 +289,8 @@ void bap_broadcast_pa_sync(esp_ble_audio_gap_app_event_t *event)
void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
{
esp_err_t err;
if (sync_handle == event->pa_sync_lost.sync_handle) {
ESP_LOGI(TAG, "PA sync %u lost with reason %u",
sync_handle, event->pa_sync_lost.reason);
@@ -261,9 +298,17 @@ void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
sync_handle = PA_SYNC_HANDLE_INIT;
pa_syncing = false; /* allow the rescan below to sync a fresh broadcaster */
if (broadcast_sink != NULL) {
esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
broadcast_sink = NULL;
/* Prefer delete from broadcast_sink_stopped_cb after bis_sync clear.
* Skip while BIG teardown is in flight; if the sink never reached
* BIG sync, delete here.
*/
if (broadcast_sink != NULL && !stop_pending) {
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
if (err) {
ESP_LOGW(TAG, "Sink delete deferred to stopped_cb, err %d", err);
} else {
broadcast_sink = NULL;
}
}
bap_broadcast_sink_scan();
@@ -33,7 +33,11 @@ static uint8_t codec_meta[] =
static const esp_ble_audio_codec_cap_t lc3_codec_cap =
ESP_BLE_AUDIO_CODEC_CAP_LC3(codec_data, codec_meta);
static esp_ble_audio_pacs_cap_t cap = {
static esp_ble_audio_pacs_cap_t cap_sink = {
.codec_cap = &lc3_codec_cap,
};
static esp_ble_audio_pacs_cap_t cap_source = {
.codec_cap = &lc3_codec_cap,
};
@@ -502,7 +506,7 @@ int bap_unicast_sr_init(void)
#if CONFIG_BT_PAC_SNK
/* Register CT required capabilities */
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap);
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap_sink);
if (err) {
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
return err;
@@ -529,7 +533,7 @@ int bap_unicast_sr_init(void)
#if CONFIG_BT_PAC_SRC
/* Register CT required capabilities */
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SOURCE, &cap);
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SOURCE, &cap_source);
if (err) {
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
return err;