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/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/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..403737e889fc 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 */ @@ -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,13 +55,14 @@ 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 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; @@ -84,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" @@ -119,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; @@ -192,75 +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, ++source_streams[i].len_to_send); - - 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 { - printk("Sending mock data with len %zu on streams[%zu] (%p)\n", - source_streams[i].len_to_send, i, stream); - } - - if (source_streams[i].len_to_send >= source_streams[i].max_sdu) { - source_streams[i].len_to_send = 0; - } - } - -#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++) { @@ -270,7 +176,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 +197,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 +266,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,18 +327,10 @@ 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; } } - 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; } @@ -509,18 +400,28 @@ 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; + 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 +434,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 +443,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); + } } } @@ -554,13 +460,30 @@ 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) { 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; + } 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); + } + } } static void stream_enabled_cb(struct bt_bap_stream *stream) @@ -747,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); @@ -754,7 +681,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++) { @@ -791,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); @@ -801,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); @@ -814,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 */ 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"