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

This commit is contained in:
Liu Linyan
2026-05-06 09:31:07 +08:00
parent 92288a2d84
commit 325b08e023
108 changed files with 3051 additions and 1269 deletions
@@ -5,57 +5,77 @@
(See the README.md file in the upper level `examples` directory for more information about examples.)
This example demonstrates the **Basic Audio Profile (BAP) Broadcast Source** functionality. It starts extended advertising with the Broadcast Audio service data (UUID and Broadcast ID) and device name, periodic advertising with the Broadcast Audio Source Endpoint (BASE), then starts the BAP Broadcast Source so that BIGInfo and (mock) audio data are sent over the BIG. Sinks such as the [broadcast_sink](../broadcast_sink) example can discover this source, establish periodic sync, and synchronize to the BIG to receive the streams.
## Overview
The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (BAP broadcast source create/start, stream send, LC3 presets). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). The source is configured with the LC3 16_2_1 broadcast preset, a hardcoded broadcast ID (`0x123456`), and a hardcoded broadcast code (`1234`) for encrypted broadcasts. These values can be changed by editing the source code constants.
This example acts as a **BAP Broadcast Source**. It creates a BAP broadcast source from the LC3 `16_2_1` broadcast preset, encodes the BASE into the periodic advertising payload, places the Broadcast Audio Announcement Service UUID and a fixed 24-bit Broadcast ID (`0x123456`) in the extended advertising payload, and then starts the BIG so that BIS streams transmit synthetic SDU data on a fixed cadence.
The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Source-side APIs used: `esp_ble_audio_common_init` / `esp_ble_audio_common_start` (common layer), `esp_ble_audio_bap_broadcast_source_create` / `_get_base` / `_start` (BAP), `esp_ble_audio_bap_broadcast_adv_add` (BAP/ISO glue), and the BAP stream callbacks (`started`, `stopped`, `sent`, `disconnected`). PACS, GAP scanner, and the scan delegator are **not** used on this side. Encryption is enabled because the broadcast code `"1234"` is non-empty; packing is `ESP_BLE_ISO_PACKING_SEQUENTIAL`. Channel allocation per stream is hard-coded as `FRONT_LEFT` for stream 0 and `FRONT_RIGHT` for stream 1.
The TX scheduler is built on `example_audio_tx_scheduler_*` helpers; the source comment notes that ESP timer resolution is not accurate enough for the SDU interval, so a `k_work_delayable`-based scheduler is used instead.
## Requirements
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
* Optionally, a device running the [broadcast_sink](../broadcast_sink) example to receive and play the broadcast streams
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* A peer running the [broadcast_sink](../broadcast_sink) example to receive and decode the BIS streams
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
Open menuconfig:
```bash
idf.py set-target esp32h4
idf.py menuconfig
```
### Build and Flash
No build-time options — runtime defaults are baked into source. The device name (`"BAP Broadcast Source"`), broadcast ID (`0x123456`), broadcast code (`"1234"`), advertising/periodic intervals, stream and subgroup counts (via `CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT` / `_SUBGROUP_COUNT` from the BAP component), and LC3 preset (`16_2_1`) are compile-time constants in `main.c`.
Run the following to build, flash and monitor:
### Security & Pairing
The example inherits a Just-Works pairing model (LE Secure Connections, no MITM, no I/O capability) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`; change pairing/IO-cap there if needed.
## Build & Flash
```bash
idf.py set-target esp32h4 # or esp32s31
idf.py -p PORT flash monitor
```
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`). No PACS or GAP callback needed for the source role.
2. **Broadcast source setup**: Register broadcast source callbacks (started/stopped). Configure subgroups and streams using the selected LC3 preset (codec config, QoS, channel allocation). Create the BAP Broadcast Source (`esp_ble_audio_bap_broadcast_source_create`) with optional encryption and sequential packing. Register stream callbacks (started, stopped, sent) for each stream.
3. **Extended and periodic advertising**: Set extended advertising data: Broadcast Audio UUID + Broadcast ID (static or random) + complete device name. Set periodic advertising data: Broadcast Audio UUID + encoded BASE from the broadcast source. Start periodic advertising then extended advertising.
4. **Start BIG**: Add the advertising set for BIG and start the BAP Broadcast Source (`esp_ble_audio_bap_broadcast_source_start`) with the same adv handle. BIGInfo is sent in the periodic advertising; BIS streams are created.
5. **Stream and send**: When each BIS stream starts, the stream started callback allocates an SDU buffer and starts a periodic TX scheduler based on `k_work_delayable`. The scheduler posts work items to the ISO task at the stream QoS interval to send mock audio data; sent and drift are reported in the stream sent callback. When a stream stops, resources are freed and the scheduler is stopped. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations.
1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(NULL)` (no GAP callback for this role).
2. `broadcast_source_setup()` registers `source_started` / `source_stopped` callbacks, populates per-stream channel-allocation BIS metadata (left/right), registers `stream_ops` on each stream, and calls `esp_ble_audio_bap_broadcast_source_create()` with the encrypted preset.
3. `esp_ble_audio_common_start(NULL)` starts the audio stack; each stream's `example_audio_tx_scheduler_init()` is wired with `tx_scheduler_cb`.
4. `ext_adv_start()` configures non-connectable extended adv (1M primary / 2M secondary, 200 ms interval), writes Broadcast Audio Service Data + Broadcast ID + complete name, configures periodic adv (100 ms), writes the BASE returned by `esp_ble_audio_bap_broadcast_source_get_base()`, then starts periodic and extended advertising.
5. `broadcast_start()` calls `esp_ble_audio_bap_broadcast_adv_add()` and `esp_ble_audio_bap_broadcast_source_start()` on the same `ADV_HANDLE`, which kicks off BIGInfo + BIS creation.
6. When each BIS stream goes streaming, `stream_started_cb` allocates an SDU-sized buffer and calls `example_audio_tx_scheduler_start()` at `preset_active.qos.interval`; the scheduler invokes `broadcast_source_tx()`, which fills the buffer with the low byte of `seq_num` and calls `esp_ble_audio_bap_stream_send()`.
7. `stream_sent_cb` forwards completions to `example_audio_tx_scheduler_on_sent()` for drift accounting; `stream_stopped_cb` and `stream_disconnected_cb` stop the scheduler; `source_stopped_cb` frees all per-stream buffers.
## Example Output
## Expected Log
```
I (xxx) BAP_BSRC: Creating broadcast source with 1 subgroups & 2 streams per subgroup
I (xxx) BAP_BSRC: Extended adv instance 0 started
I (xxx) BAP_BSRC: Broadcast source 0x... started
I (xxx) BAP_BSRC: Stream 0x... started
I (xxx) BAP_BSRC: Transmitted 1000 ISO data packets (stream 0x...)
...
I (xxx) BAP_BSRC: Creating broadcast source: ... subgroup(s), ... stream(s)/subgroup
I (xxx) BAP_BSRC: Advertising started (handle 0)
I (xxx) BAP_BSRC: Broadcast source started
I (xxx) BAP_BSRC: [SRC #0] Stream started
I (xxx) BAP_BSRC: [SRC #1] Stream started
```
If the broadcast source stops (e.g. stream stopped):
Periodic TX accounting (emitted by `example_audio_tx_scheduler_on_sent` under the `BAP_BSRC` tag, name `SRC #<idx>`) follows. On teardown:
```
I (xxx) BAP_BSRC: Stream 0x... stopped, reason 0x...
I (xxx) BAP_BSRC: Broadcast source 0x... stopped, reason 0x...
I (xxx) BAP_BSRC: [SRC #0] Stream stopped, reason 0x...
I (xxx) BAP_BSRC: [SRC #0] ISO disconnected, reason 0x...
I (xxx) BAP_BSRC: Broadcast source stopped, reason 0x...
```
## Peer Pairing
Run [broadcast_sink](../broadcast_sink/) on a second board. Expected interaction sequence:
1. Source advertises extended PDU containing the Broadcast Audio Announcement Service UUID, Broadcast ID `0x123456`, and complete name `"BAP Broadcast Source"`; sink scans and matches by name + UUID.
2. Source's periodic advertising carries the encoded BASE; sink establishes PA sync and parses the BASE to recover subgroup count and BIS index bitfield.
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.
@@ -66,24 +66,36 @@ static esp_ble_audio_bap_broadcast_source_t *broadcast_source;
static void broadcast_source_tx(struct broadcast_source_stream *source_stream);
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
if (&streams[i].stream == stream) {
return (int)i;
}
}
return -1;
}
static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
{
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
struct broadcast_source_stream,
stream);
int idx = stream_index(stream);
esp_err_t err;
ESP_LOGI(TAG, "Stream %p started", stream);
ESP_LOGI(TAG, "[SRC #%d] Stream started", idx);
if (source_stream->stream.qos == NULL || source_stream->stream.qos->sdu == 0) {
ESP_LOGE(TAG, "Invalid stream qos");
ESP_LOGE(TAG, "[SRC #%d] Invalid QoS", idx);
return;
}
if (source_stream->data == NULL) {
source_stream->data = calloc(1, source_stream->stream.qos->sdu);
if (source_stream->data == NULL) {
ESP_LOGE(TAG, "Failed to alloc tx buffer, sdu %u", source_stream->stream.qos->sdu);
ESP_LOGE(TAG, "[SRC #%d] Failed to alloc TX buffer (sdu %u)",
idx, source_stream->stream.qos->sdu);
return;
}
}
@@ -94,7 +106,7 @@ static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
/* Note: esp timer is not accurate enough */
err = example_audio_tx_scheduler_start(&source_stream->scheduler, preset_active.qos.interval);
if (err) {
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
ESP_LOGE(TAG, "[SRC #%d] Scheduler start failed, err %d", idx, err);
return;
}
@@ -106,13 +118,14 @@ static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
struct broadcast_source_stream,
stream);
int idx = stream_index(stream);
esp_err_t err;
ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[SRC #%d] Stream stopped, reason 0x%02x", idx, reason);
err = example_audio_tx_scheduler_stop(&source_stream->scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
ESP_LOGE(TAG, "[SRC #%d] Scheduler stop failed, err %d", idx, err);
}
}
@@ -121,13 +134,14 @@ static void stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t r
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
struct broadcast_source_stream,
stream);
int idx = stream_index(stream);
esp_err_t err;
ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[SRC #%d] ISO disconnected, reason 0x%02x", idx, reason);
err = example_audio_tx_scheduler_stop(&source_stream->scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
ESP_LOGE(TAG, "[SRC #%d] Scheduler stop failed, err %d", idx, err);
}
}
@@ -136,8 +150,10 @@ static void stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
struct broadcast_source_stream,
stream);
char name[24];
example_audio_tx_scheduler_on_sent(&source_stream->scheduler, user_data, TAG, "stream", stream);
snprintf(name, sizeof(name), "SRC #%d", stream_index(stream));
example_audio_tx_scheduler_on_sent(&source_stream->scheduler, user_data, TAG, name);
}
static esp_ble_audio_bap_stream_ops_t stream_ops = {
@@ -170,13 +186,16 @@ static void broadcast_source_tx(struct broadcast_source_stream *source_stream)
return;
}
int idx = stream_index(&source_stream->stream);
if (source_stream->stream.qos == NULL || source_stream->stream.qos->sdu == 0) {
ESP_LOGE(TAG, "Invalid stream qos");
ESP_LOGE(TAG, "[SRC #%d] Invalid QoS", idx);
return;
}
if (source_stream->data == NULL) {
ESP_LOGE(TAG, "Tx buffer unavailable, sdu %u", source_stream->stream.qos->sdu);
ESP_LOGE(TAG, "[SRC #%d] TX buffer unavailable (sdu %u)",
idx, source_stream->stream.qos->sdu);
return;
}
@@ -187,8 +206,7 @@ static void broadcast_source_tx(struct broadcast_source_stream *source_stream)
source_stream->stream.qos->sdu,
source_stream->seq_num);
if (err) {
ESP_LOGD(TAG, "Failed to broadcast data on stream %p, err %d",
&source_stream->stream, err);
ESP_LOGD(TAG, "[SRC #%d] send failed, err %d", idx, err);
return;
}
@@ -208,12 +226,12 @@ static void tx_scheduler_cb(void *arg)
static void source_started_cb(esp_ble_audio_bap_broadcast_source_t *source)
{
ESP_LOGI(TAG, "Broadcast source %p started", source);
ESP_LOGI(TAG, "Broadcast source started");
}
static void source_stopped_cb(esp_ble_audio_bap_broadcast_source_t *source, uint8_t reason)
{
ESP_LOGI(TAG, "Broadcast source %p stopped, reason 0x%02x", source, reason);
ESP_LOGI(TAG, "Broadcast source stopped, reason 0x%02x", reason);
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
if (streams[i].data != NULL) {
@@ -275,7 +293,7 @@ static esp_err_t broadcast_source_setup(void)
memcpy(create_param.broadcast_code, LOCAL_BROADCAST_CODE, strlen(LOCAL_BROADCAST_CODE));
}
ESP_LOGI(TAG, "Creating broadcast source with %u subgroups & %u streams per subgroup",
ESP_LOGI(TAG, "Creating broadcast source: %u subgroup(s), %u stream(s)/subgroup",
ARRAY_SIZE(subgroup_param), streams_per_subgroup);
err = esp_ble_audio_bap_broadcast_source_create(&create_param, &broadcast_source);
@@ -453,7 +471,7 @@ static int ext_adv_start(void)
goto end;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
end:
if (ext_data) {
@@ -525,7 +543,7 @@ void app_main(void)
tx_scheduler_cb,
&streams[i]);
if (err) {
ESP_LOGE(TAG, "Failed to initialize tx scheduler[%u], err %d", i, err);
ESP_LOGE(TAG, "[SRC #%zu] Scheduler init failed, err %d", i, err);
return;
}
}