From bc25ddb8c5e3da25236c49e6cc3ad2911cdd4107 Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Fri, 16 May 2025 00:45:42 +0200 Subject: [PATCH 1/3] samples: Bluetooth: BAP Unicast Client improve reporting Make the reporting interval configurable to make it easier to actually follow. Improve what gets reported. Add check for SDU size to ensure correctness when not using LIBLC3. Signed-off-by: Emil Gydesen --- samples/bluetooth/bap_unicast_client/Kconfig | 13 +++++++++++ .../bluetooth/bap_unicast_client/src/main.c | 12 +++++++--- .../bap_unicast_client/src/stream_lc3.c | 4 +++- .../bap_unicast_client/src/stream_tx.c | 22 ++++++++++++++++++- .../audio_samples/bap_unicast_client/Kconfig | 4 ++++ 5 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 samples/bluetooth/bap_unicast_client/Kconfig create mode 100644 tests/bsim/bluetooth/audio_samples/bap_unicast_client/Kconfig diff --git a/samples/bluetooth/bap_unicast_client/Kconfig b/samples/bluetooth/bap_unicast_client/Kconfig new file mode 100644 index 000000000000..49f13cd723ff --- /dev/null +++ b/samples/bluetooth/bap_unicast_client/Kconfig @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +mainmenu "Bluetooth: BAP Unicast Client" + +config INFO_REPORTING_INTERVAL + int "Number of SDUs received between each information report" + default 1000 + help + Determines how often information about received data is logged. + Set to 0 to disable reporting. + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/bap_unicast_client/src/main.c b/samples/bluetooth/bap_unicast_client/src/main.c index fdc0ed249ff9..92745fe9c140 100644 --- a/samples/bluetooth/bap_unicast_client/src/main.c +++ b/samples/bluetooth/bap_unicast_client/src/main.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024 Nordic Semiconductor ASA + * Copyright (c) 2021-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -270,6 +270,8 @@ static void stream_connected_cb(struct bt_bap_stream *stream) static void stream_started(struct bt_bap_stream *stream) { printk("Audio Stream %p started\n", stream); + unicast_audio_recv_ctr = 0U; + /* Register the stream for TX if it can send */ if (IS_ENABLED(CONFIG_BT_AUDIO_TX) && stream_tx_can_send(stream)) { const int err = stream_tx_register(stream); @@ -317,8 +319,12 @@ static void stream_recv(struct bt_bap_stream *stream, { if (info->flags & BT_ISO_FLAGS_VALID) { unicast_audio_recv_ctr++; - printk("Incoming audio on stream %p len %u (%"PRIu64")\n", stream, buf->len, - unicast_audio_recv_ctr); + + if (CONFIG_INFO_REPORTING_INTERVAL > 0 && + (unicast_audio_recv_ctr % CONFIG_INFO_REPORTING_INTERVAL) == 0U) { + printk("Incoming audio on stream %p len %u (%" PRIu64 ")\n", stream, + buf->len, unicast_audio_recv_ctr); + } } } diff --git a/samples/bluetooth/bap_unicast_client/src/stream_lc3.c b/samples/bluetooth/bap_unicast_client/src/stream_lc3.c index ab61fc5627f6..c87764a58d51 100644 --- a/samples/bluetooth/bap_unicast_client/src/stream_lc3.c +++ b/samples/bluetooth/bap_unicast_client/src/stream_lc3.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Nordic Semiconductor ASA + * Copyright (c) 2024-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -158,6 +158,7 @@ static bool encode_frame_block(struct tx_stream *stream, struct net_buf *out_buf * purposes */ if (!encode_frame(stream, i, out_buf)) { + LOG_WRN("Failed to encode frame %u", i); return false; } } @@ -169,6 +170,7 @@ void stream_lc3_add_data(struct tx_stream *stream, struct net_buf *buf) { for (uint8_t i = 0U; i < stream->lc3_tx.frame_blocks_per_sdu; i++) { if (!encode_frame_block(stream, buf)) { + LOG_WRN("Failed to encode frame block %u", i); break; } } diff --git a/samples/bluetooth/bap_unicast_client/src/stream_tx.c b/samples/bluetooth/bap_unicast_client/src/stream_tx.c index ebdc01ab87f9..ce9018e0d231 100644 --- a/samples/bluetooth/bap_unicast_client/src/stream_tx.c +++ b/samples/bluetooth/bap_unicast_client/src/stream_tx.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Nordic Semiconductor ASA + * Copyright (c) 2024-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -79,6 +80,7 @@ static void tx_thread_func(void *arg1, void *arg2, void *arg3) struct bt_bap_stream *bap_stream = tx_streams[i].bap_stream; if (stream_is_streaming(bap_stream)) { + uint16_t sdu_len; struct net_buf *buf; buf = net_buf_alloc(&tx_pool, K_FOREVER); @@ -88,12 +90,25 @@ static void tx_thread_func(void *arg1, void *arg2, void *arg3) bap_stream->codec_cfg->id == BT_HCI_CODING_FORMAT_LC3) { stream_lc3_add_data(&tx_streams[i], buf); } else { + __ASSERT(bap_stream->qos->sdu <= ARRAY_SIZE(mock_data), + "Configured codec SDU len %u does not match mock " + "data size %zu", + bap_stream->qos->sdu, ARRAY_SIZE(mock_data)); net_buf_add_mem(buf, mock_data, bap_stream->qos->sdu); } + sdu_len = buf->len; + err = bt_bap_stream_send(bap_stream, buf, tx_streams[i].seq_num); if (err == 0) { tx_streams[i].seq_num++; + + if (CONFIG_INFO_REPORTING_INTERVAL > 0 && + (tx_streams[i].seq_num % + CONFIG_INFO_REPORTING_INTERVAL) == 0U) { + LOG_INF("Stream %p: Sent %u total SDUs of size %u", + bap_stream, tx_streams[i].seq_num, sdu_len); + } } else { LOG_ERR("Unable to send: %d", err); net_buf_unref(buf); @@ -131,6 +146,11 @@ int stream_tx_register(struct bt_bap_stream *bap_stream) } LOG_INF("Registered %p for TX", bap_stream); + if (bap_stream->qos->sdu > CONFIG_BT_ISO_TX_MTU) { + LOG_WRN("Stream configured for SDUs larger (%u) than " + "CONFIG_BT_ISO_TX_MTU (%d)", + bap_stream->qos->sdu, CONFIG_BT_ISO_TX_MTU); + } return 0; } diff --git a/tests/bsim/bluetooth/audio_samples/bap_unicast_client/Kconfig b/tests/bsim/bluetooth/audio_samples/bap_unicast_client/Kconfig new file mode 100644 index 000000000000..3b3fd9ff1a4c --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/bap_unicast_client/Kconfig @@ -0,0 +1,4 @@ +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "${ZEPHYR_BASE}/samples/bluetooth/bap_unicast_client/Kconfig" From 06aa4a4d554eddf1fe195c8d7783da6c3360ef27 Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Fri, 16 May 2025 00:47:56 +0200 Subject: [PATCH 2/3] samples: Bluetooth: BAP Unicast Server: Improve reporting and TX Improve reporting by introducing a configurable reporting interval and make the report text more useful. Change TX to always send the same size, as that is what is expected/required by LE Audio. Signed-off-by: Emil Gydesen --- samples/bluetooth/bap_unicast_server/Kconfig | 13 ++++ .../bluetooth/bap_unicast_server/src/main.c | 70 +++++++++++-------- 2 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 samples/bluetooth/bap_unicast_server/Kconfig diff --git a/samples/bluetooth/bap_unicast_server/Kconfig b/samples/bluetooth/bap_unicast_server/Kconfig new file mode 100644 index 000000000000..49f13cd723ff --- /dev/null +++ b/samples/bluetooth/bap_unicast_server/Kconfig @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +mainmenu "Bluetooth: BAP Unicast Client" + +config INFO_REPORTING_INTERVAL + int "Number of SDUs received between each information report" + default 1000 + help + Determines how often information about received data is logged. + Set to 0 to disable reporting. + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/bap_unicast_server/src/main.c b/samples/bluetooth/bap_unicast_server/src/main.c index cdf3bb01fe69..8abff2a80f8e 100644 --- a/samples/bluetooth/bap_unicast_server/src/main.c +++ b/samples/bluetooth/bap_unicast_server/src/main.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024 Nordic Semiconductor ASA + * Copyright (c) 2021-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -54,12 +54,14 @@ static const struct bt_audio_codec_cap lc3_codec_cap = BT_AUDIO_CODEC_CAP_LC3( static struct bt_conn *default_conn; static struct k_work_delayable audio_send_work; -static struct bt_bap_stream sink_streams[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT]; +static struct audio_sink { + struct bt_bap_stream stream; + size_t recv_cnt; +} sink_streams[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT]; static struct audio_source { struct bt_bap_stream stream; uint16_t seq_num; - uint16_t max_sdu; - size_t len_to_send; + size_t send_cnt; } source_streams[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT]; static size_t configured_source_stream_count; @@ -237,7 +239,7 @@ static void audio_timer_timeout(struct k_work *work) } net_buf_reserve(buf, BT_ISO_CHAN_SEND_RESERVE); - net_buf_add_mem(buf, buf_data, ++source_streams[i].len_to_send); + net_buf_add_mem(buf, buf_data, stream->qos->sdu); ret = bt_bap_stream_send(stream, buf, get_and_incr_seq_num(stream)); if (ret < 0) { @@ -245,12 +247,13 @@ static void audio_timer_timeout(struct k_work *work) i, stream, ret); net_buf_unref(buf); } else { - printk("Sending mock data with len %zu on streams[%zu] (%p)\n", - source_streams[i].len_to_send, i, stream); - } + source_streams[i].send_cnt++; - if (source_streams[i].len_to_send >= source_streams[i].max_sdu) { - source_streams[i].len_to_send = 0; + if (CONFIG_INFO_REPORTING_INTERVAL > 0 && + (source_streams[i].send_cnt % CONFIG_INFO_REPORTING_INTERVAL) == 0U) { + printk("Stream %p: Sent %u total SDUs of size %u\n", stream, + source_streams[i].send_cnt, stream->qos->sdu); + } } } @@ -270,7 +273,7 @@ static enum bt_audio_dir stream_dir(const struct bt_bap_stream *stream) } for (size_t i = 0U; i < ARRAY_SIZE(sink_streams); i++) { - if (stream == &sink_streams[i]) { + if (stream == &sink_streams[i].stream) { return BT_AUDIO_DIR_SINK; } } @@ -291,7 +294,7 @@ static struct bt_bap_stream *stream_alloc(enum bt_audio_dir dir) } } else { for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) { - struct bt_bap_stream *stream = &sink_streams[i]; + struct bt_bap_stream *stream = &sink_streams[i].stream; if (!stream->conn) { return stream; @@ -360,13 +363,6 @@ static int lc3_qos(struct bt_bap_stream *stream, const struct bt_bap_qos_cfg *qo print_qos(qos); - for (size_t i = 0U; i < configured_source_stream_count; i++) { - if (stream == &source_streams[i].stream) { - source_streams[i].max_sdu = qos->sdu; - break; - } - } - return 0; } @@ -428,7 +424,6 @@ static int lc3_start(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) for (size_t i = 0U; i < configured_source_stream_count; i++) { if (stream == &source_streams[i].stream) { source_streams[i].seq_num = 0U; - source_streams[i].len_to_send = 0U; break; } } @@ -509,18 +504,26 @@ static void stream_recv_lc3_codec(struct bt_bap_stream *stream, const struct bt_iso_recv_info *info, struct net_buf *buf) { + struct audio_sink *sink_stream = CONTAINER_OF(stream, struct audio_sink, stream); const bool valid_data = (info->flags & BT_ISO_FLAGS_VALID) != 0; const int octets_per_frame = buf->len / frames_per_sdu; + if (valid_data) { + sink_stream->recv_cnt++; + + if (CONFIG_INFO_REPORTING_INTERVAL > 0 && + (sink_stream->recv_cnt % CONFIG_INFO_REPORTING_INTERVAL) == 0U) { + printk("Incoming audio on stream %p len %u\n", stream, buf->len); + } + } else { + printk("Bad packet: 0x%02X\n", info->flags); + } + if (lc3_decoder == NULL) { printk("LC3 decoder not setup, cannot decode data.\n"); return; } - if (!valid_data) { - printk("Bad packet: 0x%02X\n", info->flags); - } - for (int i = 0; i < frames_per_sdu; i++) { /* Passing NULL performs PLC */ const int err = lc3_decode( @@ -533,8 +536,6 @@ static void stream_recv_lc3_codec(struct bt_bap_stream *stream, printk("[%d]: Decoder failed - wrong parameters?: %d\n", i, err); } } - - printk("RX stream %p len %u\n", stream, buf->len); } #else @@ -544,7 +545,14 @@ static void stream_recv(struct bt_bap_stream *stream, struct net_buf *buf) { if (info->flags & BT_ISO_FLAGS_VALID) { - printk("Incoming audio on stream %p len %u\n", stream, buf->len); + struct audio_sink *sink_stream = CONTAINER_OF(stream, struct audio_sink, stream); + + sink_stream->recv_cnt++; + + if (CONFIG_INFO_REPORTING_INTERVAL > 0 && + (sink_stream->recv_cnt % CONFIG_INFO_REPORTING_INTERVAL) == 0U) { + printk("Incoming audio on stream %p len %u\n", stream, buf->len); + } } } @@ -561,6 +569,12 @@ static void stream_stopped(struct bt_bap_stream *stream, uint8_t reason) static void stream_started(struct bt_bap_stream *stream) { printk("Audio Stream %p started\n", stream); + + if (stream_dir(stream) == BT_AUDIO_DIR_SINK) { + struct audio_sink *sink_stream = CONTAINER_OF(stream, struct audio_sink, stream); + + sink_stream->recv_cnt = 0U; + } } static void stream_enabled_cb(struct bt_bap_stream *stream) @@ -754,7 +768,7 @@ int main(void) bt_pacs_cap_register(BT_AUDIO_DIR_SOURCE, &cap_source); for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) { - bt_bap_stream_cb_register(&sink_streams[i], &stream_ops); + bt_bap_stream_cb_register(&sink_streams[i].stream, &stream_ops); } for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) { From ddd8bc1aaa60a4585fada50b88247b2259c3749f Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Fri, 16 May 2025 11:43:08 +0200 Subject: [PATCH 3/3] samples: Bluetooth: BAP: Unicast Server: Refactor TX Refactors the TX of the unicast server. This commit effectively copies the TX approach of the unicast client sample, to make the sample more similar. This changes the TX to be based on the number of complete packets event (which results in free'ing the buffer), instead of a timer. This completely ensures that we do not skip any sequence numbers which may be rejected by the controller. Signed-off-by: Emil Gydesen --- .../bap_unicast_server/CMakeLists.txt | 3 + .../bluetooth/bap_unicast_server/src/main.c | 138 ++---------- .../bap_unicast_server/src/stream_lc3.c | 208 ++++++++++++++++++ .../bap_unicast_server/src/stream_lc3.h | 53 +++++ .../bap_unicast_server/src/stream_tx.c | 196 +++++++++++++++++ .../bap_unicast_server/src/stream_tx.h | 60 +++++ 6 files changed, 541 insertions(+), 117 deletions(-) create mode 100644 samples/bluetooth/bap_unicast_server/src/stream_lc3.c create mode 100644 samples/bluetooth/bap_unicast_server/src/stream_lc3.h create mode 100644 samples/bluetooth/bap_unicast_server/src/stream_tx.c create mode 100644 samples/bluetooth/bap_unicast_server/src/stream_tx.h diff --git a/samples/bluetooth/bap_unicast_server/CMakeLists.txt b/samples/bluetooth/bap_unicast_server/CMakeLists.txt index 66649d87a33c..91e5bf5ed7af 100644 --- a/samples/bluetooth/bap_unicast_server/CMakeLists.txt +++ b/samples/bluetooth/bap_unicast_server/CMakeLists.txt @@ -8,4 +8,7 @@ target_sources(app PRIVATE src/main.c ) +zephyr_sources_ifdef(CONFIG_LIBLC3 src/stream_lc3.c) +zephyr_sources_ifdef(CONFIG_BT_AUDIO_TX src/stream_tx.c) + zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) diff --git a/samples/bluetooth/bap_unicast_server/src/main.c b/samples/bluetooth/bap_unicast_server/src/main.c index 8abff2a80f8e..403737e889fc 100644 --- a/samples/bluetooth/bap_unicast_server/src/main.c +++ b/samples/bluetooth/bap_unicast_server/src/main.c @@ -32,6 +32,8 @@ #include #include +#include "stream_tx.h" + #define AVAILABLE_SINK_CONTEXT (BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \ BT_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \ BT_AUDIO_CONTEXT_TYPE_MEDIA | \ @@ -53,7 +55,6 @@ static const struct bt_audio_codec_cap lc3_codec_cap = BT_AUDIO_CODEC_CAP_LC3( (BT_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | BT_AUDIO_CONTEXT_TYPE_MEDIA)); static struct bt_conn *default_conn; -static struct k_work_delayable audio_send_work; static struct audio_sink { struct bt_bap_stream stream; size_t recv_cnt; @@ -86,33 +87,6 @@ static const struct bt_data ad[] = { BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1), }; -#define AUDIO_DATA_TIMEOUT_US 1000000UL /* Send data every 1 second */ -#define SDU_INTERVAL_US 10000UL /* 10 ms SDU interval */ - -static uint16_t get_and_incr_seq_num(const struct bt_bap_stream *stream) -{ - for (size_t i = 0U; i < configured_source_stream_count; i++) { - if (stream == &source_streams[i].stream) { - uint16_t seq_num; - - seq_num = source_streams[i].seq_num; - - if (IS_ENABLED(CONFIG_LIBLC3)) { - source_streams[i].seq_num++; - } else { - source_streams[i].seq_num += (AUDIO_DATA_TIMEOUT_US / - SDU_INTERVAL_US); - } - - return seq_num; - } - } - - printk("Could not find endpoint from stream %p\n", stream); - - return 0; -} - #if defined(CONFIG_LIBLC3) #include "lc3.h" @@ -121,7 +95,6 @@ static uint16_t get_and_incr_seq_num(const struct bt_bap_stream *stream) #define MAX_FRAME_DURATION_US 10000 #define MAX_NUM_SAMPLES ((MAX_FRAME_DURATION_US * MAX_SAMPLE_RATE) / USEC_PER_SEC) -static int16_t audio_buf[MAX_NUM_SAMPLES]; static lc3_decoder_t lc3_decoder; static lc3_decoder_mem_48k_t lc3_decoder_mem; static int frames_per_sdu; @@ -194,76 +167,6 @@ static void print_qos(const struct bt_bap_qos_cfg *qos) qos->rtn, qos->latency, qos->pd); } -/** - * @brief Send audio data on timeout - * - * This will send an increasing amount of audio data, starting from 1 octet. - * The data is just mock data, and does not actually represent any audio. - * - * First iteration : 0x00 - * Second iteration: 0x00 0x01 - * Third iteration : 0x00 0x01 0x02 - * - * And so on, until it wraps around the configured MTU (CONFIG_BT_ISO_TX_MTU) - * - * @param work Pointer to the work structure - */ -static void audio_timer_timeout(struct k_work *work) -{ - int ret; - static uint8_t buf_data[CONFIG_BT_ISO_TX_MTU]; - static bool data_initialized; - struct net_buf *buf; - - if (!data_initialized) { - /* TODO: Actually encode some audio data */ - for (size_t i = 0U; i < ARRAY_SIZE(buf_data); i++) { - buf_data[i] = (uint8_t)i; - } - - data_initialized = true; - } - - /* We configured the sink streams to be first in `streams`, so that - * we can use `stream[i]` to select sink streams (i.e. streams with - * data going to the server) - */ - for (size_t i = 0; i < configured_source_stream_count; i++) { - struct bt_bap_stream *stream = &source_streams[i].stream; - - buf = net_buf_alloc(&tx_pool, K_NO_WAIT); - if (buf == NULL) { - printk("Failed to allocate TX buffer\n"); - /* Break and retry later */ - break; - } - net_buf_reserve(buf, BT_ISO_CHAN_SEND_RESERVE); - - net_buf_add_mem(buf, buf_data, stream->qos->sdu); - - ret = bt_bap_stream_send(stream, buf, get_and_incr_seq_num(stream)); - if (ret < 0) { - printk("Failed to send audio data on streams[%zu] (%p): (%d)\n", - i, stream, ret); - net_buf_unref(buf); - } else { - source_streams[i].send_cnt++; - - if (CONFIG_INFO_REPORTING_INTERVAL > 0 && - (source_streams[i].send_cnt % CONFIG_INFO_REPORTING_INTERVAL) == 0U) { - printk("Stream %p: Sent %u total SDUs of size %u\n", stream, - source_streams[i].send_cnt, stream->qos->sdu); - } - } - } - -#if defined(CONFIG_LIBLC3) - k_work_schedule(&audio_send_work, K_USEC(MAX_FRAME_DURATION_US)); -#else - k_work_schedule(&audio_send_work, K_USEC(AUDIO_DATA_TIMEOUT_US)); -#endif -} - static enum bt_audio_dir stream_dir(const struct bt_bap_stream *stream) { for (size_t i = 0U; i < ARRAY_SIZE(source_streams); i++) { @@ -428,13 +331,6 @@ static int lc3_start(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) } } - if (configured_source_stream_count > 0 && - !k_work_delayable_is_pending(&audio_send_work)) { - - /* Start send timer */ - k_work_schedule(&audio_send_work, K_MSEC(0)); - } - return 0; } @@ -504,6 +400,8 @@ static void stream_recv_lc3_codec(struct bt_bap_stream *stream, const struct bt_iso_recv_info *info, struct net_buf *buf) { + static int16_t audio_buf[MAX_NUM_SAMPLES]; + struct audio_sink *sink_stream = CONTAINER_OF(stream, struct audio_sink, stream); const bool valid_data = (info->flags & BT_ISO_FLAGS_VALID) != 0; const int octets_per_frame = buf->len / frames_per_sdu; @@ -562,8 +460,13 @@ static void stream_stopped(struct bt_bap_stream *stream, uint8_t reason) { printk("Audio Stream %p stopped with reason 0x%02X\n", stream, reason); - /* Stop send timer */ - k_work_cancel_delayable(&audio_send_work); + if (IS_ENABLED(CONFIG_BT_AUDIO_TX) && stream_dir(stream) == BT_AUDIO_DIR_SOURCE) { + const int err = stream_tx_unregister(stream); + + if (err != 0) { + printk("Failed to register stream %p for TX: %d\n", stream, err); + } + } } static void stream_started(struct bt_bap_stream *stream) @@ -574,6 +477,12 @@ static void stream_started(struct bt_bap_stream *stream) struct audio_sink *sink_stream = CONTAINER_OF(stream, struct audio_sink, stream); sink_stream->recv_cnt = 0U; + } else if (IS_ENABLED(CONFIG_BT_AUDIO_TX)) { + const int err = stream_tx_register(stream); + + if (err != 0) { + printk("Failed to register stream %p for TX: %d\n", stream, err); + } } } @@ -761,6 +670,10 @@ int main(void) return 0; } + if (IS_ENABLED(CONFIG_BT_AUDIO_TX)) { + stream_tx_init(); + } + bt_bap_unicast_server_register(¶m); bt_bap_unicast_server_register_cb(&unicast_server_cb); @@ -805,8 +718,6 @@ int main(void) } while (true) { - struct k_work_sync sync; - err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT); if (err) { printk("Failed to start advertising set (err %d)\n", err); @@ -815,11 +726,6 @@ int main(void) printk("Advertising successfully started\n"); - if (CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT > 0) { - /* Start send timer */ - k_work_init_delayable(&audio_send_work, audio_timer_timeout); - } - err = k_sem_take(&sem_disconnected, K_FOREVER); if (err != 0) { printk("failed to take sem_disconnected (err %d)\n", err); @@ -828,8 +734,6 @@ int main(void) /* reset data */ configured_source_stream_count = 0U; - k_work_cancel_delayable_sync(&audio_send_work, &sync); - } return 0; } diff --git a/samples/bluetooth/bap_unicast_server/src/stream_lc3.c b/samples/bluetooth/bap_unicast_server/src/stream_lc3.c new file mode 100644 index 000000000000..c87764a58d51 --- /dev/null +++ b/samples/bluetooth/bap_unicast_server/src/stream_lc3.c @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2024-2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "stream_lc3.h" +#include "stream_tx.h" + +LOG_MODULE_REGISTER(lc3, LOG_LEVEL_INF); + +#define LC3_MAX_SAMPLE_RATE 48000U +#define LC3_MAX_FRAME_DURATION_US 10000U +#define LC3_MAX_NUM_SAMPLES ((LC3_MAX_FRAME_DURATION_US * LC3_MAX_SAMPLE_RATE) / USEC_PER_SEC) +/* codec does clipping above INT16_MAX - 3000 */ +#define AUDIO_VOLUME (INT16_MAX - 3000) +#define AUDIO_TONE_FREQUENCY_HZ 400 + +static int16_t audio_buf[LC3_MAX_NUM_SAMPLES]; +/** + * Use the math lib to generate a sine-wave using 16 bit samples into a buffer. + * + * @param stream The TX stream to generate and fill the sine wave for + */ +static void fill_audio_buf_sin(struct tx_stream *stream) +{ + const unsigned int num_samples = + (stream->lc3_tx.frame_duration_us * stream->lc3_tx.freq_hz) / USEC_PER_SEC; + const int sine_period_samples = stream->lc3_tx.freq_hz / AUDIO_TONE_FREQUENCY_HZ; + const float step = 2 * 3.1415f / sine_period_samples; + + for (unsigned int i = 0; i < num_samples; i++) { + const float sample = sinf(i * step); + + audio_buf[i] = (int16_t)(AUDIO_VOLUME * sample); + } +} + +static int extract_lc3_config(struct tx_stream *stream) +{ + const struct bt_audio_codec_cfg *codec_cfg = stream->bap_stream->codec_cfg; + struct stream_lc3_tx *lc3_tx = &stream->lc3_tx; + int ret; + + LOG_INF("Extracting LC3 configuration values"); + + ret = bt_audio_codec_cfg_get_freq(codec_cfg); + if (ret >= 0) { + ret = bt_audio_codec_cfg_freq_to_freq_hz(ret); + if (ret > 0) { + if (LC3_CHECK_SR_HZ(ret)) { + lc3_tx->freq_hz = (uint32_t)ret; + } else { + LOG_ERR("Unsupported sampling frequency for LC3: %d", ret); + + return ret; + } + } else { + LOG_ERR("Invalid frequency: %d", ret); + + return ret; + } + } else { + LOG_ERR("Could not get frequency: %d", ret); + + return ret; + } + + ret = bt_audio_codec_cfg_get_frame_dur(codec_cfg); + if (ret >= 0) { + ret = bt_audio_codec_cfg_frame_dur_to_frame_dur_us(ret); + if (ret > 0) { + if (LC3_CHECK_DT_US(ret)) { + lc3_tx->frame_duration_us = (uint32_t)ret; + } else { + LOG_ERR("Unsupported frame duration for LC3: %d", ret); + + return ret; + } + } else { + LOG_ERR("Invalid frame duration: %d", ret); + + return ret; + } + } else { + LOG_ERR("Could not get frame duration: %d", ret); + + return ret; + } + + ret = bt_audio_codec_cfg_get_chan_allocation(codec_cfg, &lc3_tx->chan_allocation, false); + if (ret != 0) { + LOG_DBG("Could not get channel allocation: %d", ret); + lc3_tx->chan_allocation = BT_AUDIO_LOCATION_MONO_AUDIO; + } + + lc3_tx->chan_cnt = bt_audio_get_chan_count(lc3_tx->chan_allocation); + + ret = bt_audio_codec_cfg_get_frame_blocks_per_sdu(codec_cfg, true); + if (ret >= 0) { + lc3_tx->frame_blocks_per_sdu = (uint8_t)ret; + } + + ret = bt_audio_codec_cfg_get_octets_per_frame(codec_cfg); + if (ret >= 0) { + lc3_tx->octets_per_frame = (uint16_t)ret; + } else { + LOG_ERR("Could not get octets per frame: %d", ret); + + return ret; + } + + return 0; +} + +static bool encode_frame(struct tx_stream *stream, uint8_t index, struct net_buf *out_buf) +{ + const uint16_t octets_per_frame = stream->lc3_tx.octets_per_frame; + int lc3_ret; + + /* Generate sine wave */ + fill_audio_buf_sin(stream); + + lc3_ret = lc3_encode(stream->lc3_tx.encoder, LC3_PCM_FORMAT_S16, audio_buf, 1, + octets_per_frame, net_buf_tail(out_buf)); + if (lc3_ret < 0) { + LOG_ERR("LC3 encoder failed - wrong parameters?: %d", lc3_ret); + + return false; + } + + out_buf->len += octets_per_frame; + + return true; +} + +static bool encode_frame_block(struct tx_stream *stream, struct net_buf *out_buf) +{ + for (uint8_t i = 0U; i < stream->lc3_tx.chan_cnt; i++) { + /* We provide the total number of decoded frames to `decode_frame` for logging + * purposes + */ + if (!encode_frame(stream, i, out_buf)) { + LOG_WRN("Failed to encode frame %u", i); + return false; + } + } + + return true; +} + +void stream_lc3_add_data(struct tx_stream *stream, struct net_buf *buf) +{ + for (uint8_t i = 0U; i < stream->lc3_tx.frame_blocks_per_sdu; i++) { + if (!encode_frame_block(stream, buf)) { + LOG_WRN("Failed to encode frame block %u", i); + break; + } + } +} + +int stream_lc3_init(struct tx_stream *stream) +{ + int err; + + err = extract_lc3_config(stream); + if (err != 0) { + memset(&stream->lc3_tx, 0, sizeof(stream->lc3_tx)); + + return err; + } + + /* Fill audio buffer with Sine wave only once and repeat encoding the same tone frame */ + LOG_INF("Initializing sine wave data"); + fill_audio_buf_sin(stream); + + LOG_INF("Setting up LC3 encoder"); + stream->lc3_tx.encoder = + lc3_setup_encoder(stream->lc3_tx.frame_duration_us, stream->lc3_tx.freq_hz, 0, + &stream->lc3_tx.encoder_mem); + + if (stream->lc3_tx.encoder == NULL) { + LOG_ERR("Failed to setup LC3 encoder"); + + memset(&stream->lc3_tx, 0, sizeof(stream->lc3_tx)); + + return -ENOEXEC; + } + + return 0; +} diff --git a/samples/bluetooth/bap_unicast_server/src/stream_lc3.h b/samples/bluetooth/bap_unicast_server/src/stream_lc3.h new file mode 100644 index 000000000000..c323372c3298 --- /dev/null +++ b/samples/bluetooth/bap_unicast_server/src/stream_lc3.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef STREAM_LC3_H +#define STREAM_LC3_H + +#include + +#include +#include +#include + +/* Since the lc3.h header file is not available when CONFIG_LIBLC3=n, we need to guard the include + * and use of it + */ +#if defined(CONFIG_LIBLC3) +/* Header file for the liblc3 */ +#include + +struct stream_lc3_tx { + uint32_t freq_hz; + uint32_t frame_duration_us; + uint16_t octets_per_frame; + uint8_t frame_blocks_per_sdu; + uint8_t chan_cnt; + enum bt_audio_location chan_allocation; + lc3_encoder_t encoder; + lc3_encoder_mem_48k_t encoder_mem; +}; +#endif /* CONFIG_LIBLC3 */ + +/* Opaque definition to avoid including stream_tx.h */ +struct tx_stream; + +/* + * @brief Initialize LC3 encoder for a stream + * + * This will initialize the encoder for the provided TX stream + */ +int stream_lc3_init(struct tx_stream *stream); + +/** + * Add LC3 encoded data to the provided buffer from the provided stream + * + * @param stream The TX stream to add data from + * @param buf The buffer to store the encoded audio data in + */ +void stream_lc3_add_data(struct tx_stream *stream, struct net_buf *buf); + +#endif /* STREAM_LC3_H */ diff --git a/samples/bluetooth/bap_unicast_server/src/stream_tx.c b/samples/bluetooth/bap_unicast_server/src/stream_tx.c new file mode 100644 index 000000000000..9950e45596f6 --- /dev/null +++ b/samples/bluetooth/bap_unicast_server/src/stream_tx.c @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2024-2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "stream_lc3.h" +#include "stream_tx.h" + +LOG_MODULE_REGISTER(stream_tx, LOG_LEVEL_INF); + +static struct tx_stream tx_streams[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT]; + +static bool stream_is_streaming(const struct bt_bap_stream *bap_stream) +{ + struct bt_bap_ep_info ep_info; + int err; + + if (bap_stream == NULL) { + return false; + } + + /* No-op if stream is not configured */ + if (bap_stream->ep == NULL) { + return false; + } + + err = bt_bap_ep_get_info(bap_stream->ep, &ep_info); + if (err != 0) { + return false; + } + + return ep_info.state == BT_BAP_EP_STATE_STREAMING; +} + +static void tx_thread_func(void *arg1, void *arg2, void *arg3) +{ + NET_BUF_POOL_FIXED_DEFINE(tx_pool, CONFIG_BT_ISO_TX_BUF_COUNT, + BT_ISO_SDU_BUF_SIZE(CONFIG_BT_ISO_TX_MTU), + CONFIG_BT_CONN_TX_USER_DATA_SIZE, NULL); + static uint8_t mock_data[CONFIG_BT_ISO_TX_MTU]; + + for (size_t i = 0U; i < ARRAY_SIZE(mock_data); i++) { + mock_data[i] = (uint8_t)i; + } + + /* This loop will attempt to send on all streams in the streaming state in a round robin + * fashion. + * The TX is controlled by the number of buffers configured, and increasing + * CONFIG_BT_ISO_TX_BUF_COUNT will allow for more streams in parallel, or to submit more + * buffers per stream. + * Once a buffer has been freed by the stack, it triggers the next TX. + */ + while (true) { + int err = -ENOEXEC; + + for (size_t i = 0U; i < ARRAY_SIZE(tx_streams); i++) { + struct bt_bap_stream *bap_stream = tx_streams[i].bap_stream; + + if (stream_is_streaming(bap_stream)) { + uint16_t sdu_len; + struct net_buf *buf; + + buf = net_buf_alloc(&tx_pool, K_FOREVER); + net_buf_reserve(buf, BT_ISO_CHAN_SEND_RESERVE); + + if (IS_ENABLED(CONFIG_LIBLC3) && + bap_stream->codec_cfg->id == BT_HCI_CODING_FORMAT_LC3) { + stream_lc3_add_data(&tx_streams[i], buf); + } else { + __ASSERT(bap_stream->qos->sdu <= ARRAY_SIZE(mock_data), + "Configured codec SDU len %u does not match mock " + "data size %zu", + bap_stream->qos->sdu, ARRAY_SIZE(mock_data)); + net_buf_add_mem(buf, mock_data, bap_stream->qos->sdu); + } + + sdu_len = buf->len; + + err = bt_bap_stream_send(bap_stream, buf, tx_streams[i].seq_num); + if (err == 0) { + tx_streams[i].seq_num++; + + if (CONFIG_INFO_REPORTING_INTERVAL > 0 && + (tx_streams[i].seq_num % + CONFIG_INFO_REPORTING_INTERVAL) == 0U) { + LOG_INF("Stream %p: Sent %u total SDUs of size %u", + bap_stream, tx_streams[i].seq_num, sdu_len); + } + } else { + LOG_ERR("Unable to send: %d", err); + net_buf_unref(buf); + } + } /* No-op if stream is not streaming */ + } + + if (err != 0) { + /* In case of any errors, retry with a delay */ + k_sleep(K_MSEC(10)); + } + } +} + +int stream_tx_register(struct bt_bap_stream *bap_stream) +{ + if (bap_stream == NULL) { + return -EINVAL; + } + + for (size_t i = 0U; i < ARRAY_SIZE(tx_streams); i++) { + if (tx_streams[i].bap_stream == NULL) { + tx_streams[i].bap_stream = bap_stream; + tx_streams[i].seq_num = 0U; + + if (IS_ENABLED(CONFIG_LIBLC3) && + bap_stream->codec_cfg->id == BT_HCI_CODING_FORMAT_LC3) { + const int err = stream_lc3_init(&tx_streams[i]); + + if (err != 0) { + tx_streams[i].bap_stream = NULL; + + return err; + } + } + + LOG_INF("Registered %p for TX", bap_stream); + if (bap_stream->qos->sdu > CONFIG_BT_ISO_TX_MTU) { + LOG_WRN("Stream configured for SDUs larger (%u) than " + "CONFIG_BT_ISO_TX_MTU (%d)", + bap_stream->qos->sdu, CONFIG_BT_ISO_TX_MTU); + } + + return 0; + } + } + + return -ENOMEM; +} + +int stream_tx_unregister(struct bt_bap_stream *bap_stream) +{ + if (bap_stream == NULL) { + return -EINVAL; + } + + for (size_t i = 0U; i < ARRAY_SIZE(tx_streams); i++) { + if (tx_streams[i].bap_stream == bap_stream) { + tx_streams[i].bap_stream = NULL; + + LOG_INF("Unregistered %p for TX", bap_stream); + + return 0; + } + } + + return -ENODATA; +} + +void stream_tx_init(void) +{ + static bool thread_started; + + if (!thread_started) { + static K_KERNEL_STACK_DEFINE(tx_thread_stack, + IS_ENABLED(CONFIG_LIBLC3) ? 4096U : 1024U); + const int tx_thread_prio = K_PRIO_PREEMPT(5); + static struct k_thread tx_thread; + + k_thread_create(&tx_thread, tx_thread_stack, K_KERNEL_STACK_SIZEOF(tx_thread_stack), + tx_thread_func, NULL, NULL, NULL, tx_thread_prio, 0, K_NO_WAIT); + k_thread_name_set(&tx_thread, "TX thread"); + thread_started = true; + } +} diff --git a/samples/bluetooth/bap_unicast_server/src/stream_tx.h b/samples/bluetooth/bap_unicast_server/src/stream_tx.h new file mode 100644 index 000000000000..0f43d71e958a --- /dev/null +++ b/samples/bluetooth/bap_unicast_server/src/stream_tx.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef STREAM_TX_H +#define STREAM_TX_H + +#include + +#include +#include +#include +#include + +#include "stream_lc3.h" + +struct tx_stream { + struct bt_bap_stream *bap_stream; + uint16_t seq_num; + +#if defined(CONFIG_LIBLC3) + struct stream_lc3_tx lc3_tx; +#endif /* CONFIG_LIBLC3 */ +}; + +/** + * @brief Initialize TX + * + * This will initialize TX if not already initialized. This creates and starts a thread that + * will attempt to send data on all streams registered with stream_tx_register(). + */ +void stream_tx_init(void); + +/** + * @brief Register a stream for TX + * + * This will add it to the list of streams the TX thread will attempt to send on. + * + * @retval 0 on success + * @retval -EINVAL if @p bap_stream is NULL + * @retval -EINVAL if @p bap_stream.codec_cfg contains invalid values + * @retval -ENOEXEC if the LC3 encoder failed to initialize + * @retval -ENOMEM if not more streams can be registered + */ +int stream_tx_register(struct bt_bap_stream *bap_stream); + +/** + * @brief Unregister a stream for TX + * + * This will remove it to the list of streams the TX thread will attempt to send on. + * + * @retval 0 on success + * @retval -EINVAL if @p bap_stream is NULL + * @retval -EALREADY if the stream is currently not registered + */ +int stream_tx_unregister(struct bt_bap_stream *bap_stream); + +#endif /* STREAM_TX_H */