samples: Bluetooth: Refactor TX for unicast audio client

Refactored the unicast audio client sample to use a
separate thread for TXing, rather than the system workqueue.

This allows us to do blocking waiting operations like
buf = net_buf_alloc(&tx_pool, K_FOREVER);
and also splits the responsibility of TXing to a new
file as well.

LC3 handling have also been moved to a new file, so
that it does not pollute the source code for non-LC3
supported builds. This also fixes various issues and
improves the LC3 handling in the sample.

Signed-off-by: Emil Gydesen <emil.gydesen@nordicsemi.no>
This commit is contained in:
Emil Gydesen 2024-05-28 15:04:16 +02:00 committed by Johan Hedberg
commit 038fc8b6ac
10 changed files with 553 additions and 346 deletions

View file

@ -8,4 +8,7 @@ target_sources(app PRIVATE
src/main.c 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) zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth)

View file

@ -4,18 +4,21 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
#include <zephyr/types.h>
#include <stddef.h> #include <stddef.h>
#include <errno.h> #include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/audio/audio.h> #include <zephyr/bluetooth/audio/audio.h>
#include <zephyr/bluetooth/audio/bap.h> #include <zephyr/bluetooth/audio/bap.h>
#include <zephyr/bluetooth/audio/bap_lc3_preset.h> #include <zephyr/bluetooth/audio/bap_lc3_preset.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/byteorder.h> #include <zephyr/sys/byteorder.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys/util_macro.h>
#include <zephyr/types.h>
#include "stream_tx.h"
static void start_scan(void); static void start_scan(void);
@ -23,16 +26,12 @@ uint64_t unicast_audio_recv_ctr; /* This value is exposed to test code */
static struct bt_bap_unicast_client_cb unicast_client_cbs; static struct bt_bap_unicast_client_cb unicast_client_cbs;
static struct bt_conn *default_conn; static struct bt_conn *default_conn;
static struct k_work_delayable audio_send_work;
static struct bt_bap_unicast_group *unicast_group; static struct bt_bap_unicast_group *unicast_group;
static struct audio_sink { static struct audio_sink {
struct bt_bap_ep *ep; struct bt_bap_ep *ep;
uint16_t seq_num; uint16_t seq_num;
} sinks[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT]; } sinks[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT];
static struct bt_bap_ep *sources[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT]; static struct bt_bap_ep *sources[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT];
NET_BUF_POOL_FIXED_DEFINE(tx_pool, CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT,
BT_ISO_SDU_BUF_SIZE(CONFIG_BT_ISO_TX_MTU),
CONFIG_BT_CONN_TX_USER_DATA_SIZE, NULL);
static struct bt_bap_stream streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT + static struct bt_bap_stream streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT +
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT]; CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT];
@ -61,302 +60,6 @@ static K_SEM_DEFINE(sem_stream_enabled, 0, 1);
static K_SEM_DEFINE(sem_stream_started, 0, 1); static K_SEM_DEFINE(sem_stream_started, 0, 1);
static K_SEM_DEFINE(sem_stream_connected, 0, 1); static K_SEM_DEFINE(sem_stream_connected, 0, 1);
#define AUDIO_DATA_TIMEOUT_US 1000000UL /* Send data every 1 second */
static uint16_t get_and_incr_seq_num(const struct bt_bap_stream *stream)
{
for (size_t i = 0U; i < configured_sink_stream_count; i++) {
if (stream->ep == sinks[i].ep) {
uint16_t seq_num;
seq_num = sinks[i].seq_num;
if (IS_ENABLED(CONFIG_LIBLC3)) {
sinks[i].seq_num++;
} else {
sinks[i].seq_num += (AUDIO_DATA_TIMEOUT_US /
codec_configuration.qos.interval);
}
return seq_num;
}
}
printk("Could not find endpoint from stream %p\n", stream);
return 0;
}
#if defined(CONFIG_LIBLC3)
#include "lc3.h"
#include "math.h"
#define MAX_SAMPLE_RATE 48000
#define MAX_FRAME_DURATION_US 10000
#define MAX_NUM_SAMPLES ((MAX_FRAME_DURATION_US * MAX_SAMPLE_RATE) / USEC_PER_SEC)
#define AUDIO_VOLUME (INT16_MAX - 3000) /* codec does clipping above INT16_MAX - 3000 */
#define AUDIO_TONE_FREQUENCY_HZ 400
static int16_t audio_buf[MAX_NUM_SAMPLES];
static lc3_encoder_t lc3_encoder;
static lc3_encoder_mem_48k_t lc3_encoder_mem;
static int freq_hz;
static int frame_duration_us;
static int frame_duration_100us;
static int frames_per_sdu;
static int octets_per_frame;
/**
* Use the math lib to generate a sine-wave using 16 bit samples into a buffer.
*
* @param buf Destination buffer
* @param length_us Length of the buffer in microseconds
* @param frequency_hz frequency in Hz
* @param sample_rate_hz sample-rate in Hz.
*/
static void fill_audio_buf_sin(int16_t *buf, int length_us, int frequency_hz, int sample_rate_hz)
{
const int sine_period_samples = sample_rate_hz / frequency_hz;
const unsigned int num_samples = (length_us * sample_rate_hz) / USEC_PER_SEC;
const float step = 2 * 3.1415f / sine_period_samples;
for (unsigned int i = 0; i < num_samples; i++) {
const float sample = sinf(i * step);
buf[i] = (int16_t)(AUDIO_VOLUME * sample);
}
}
static void lc3_audio_timer_timeout(struct k_work *work)
{
/* For the first call-back we push multiple audio frames to the buffer to use the
* controller ISO buffer to handle jitter.
*/
const uint8_t prime_count = 2;
static int64_t start_time;
static int32_t sdu_cnt;
int32_t sdu_goal_cnt;
int64_t uptime, run_time_ms, run_time_100us;
k_work_schedule(&audio_send_work, K_USEC(codec_configuration.qos.interval));
if (lc3_encoder == NULL) {
printk("LC3 encoder not setup, cannot encode data.\n");
return;
}
if (start_time == 0) {
/* Read start time and produce the number of frames needed to catch up with any
* inaccuracies in the timer. by calculating the number of frames we should
* have sent and compare to how many were actually sent.
*/
start_time = k_uptime_get();
}
uptime = k_uptime_get();
run_time_ms = uptime - start_time;
/* PDU count calculations done in 100us units to allow 7.5ms framelength in fixed-point */
run_time_100us = run_time_ms * 10;
sdu_goal_cnt = run_time_100us / (frame_duration_100us * frames_per_sdu);
/* Add primer value to ensure the controller do not run low on data due to jitter */
sdu_goal_cnt += prime_count;
printk("LC3 encode %d frames in %d SDUs\n", (sdu_goal_cnt - sdu_cnt) * frames_per_sdu,
(sdu_goal_cnt - sdu_cnt));
while (sdu_cnt < sdu_goal_cnt) {
const uint16_t tx_sdu_len = frames_per_sdu * octets_per_frame;
struct net_buf *buf;
uint8_t *net_buffer;
off_t offset = 0;
buf = net_buf_alloc(&tx_pool, K_FOREVER);
net_buf_reserve(buf, BT_ISO_CHAN_SEND_RESERVE);
net_buffer = net_buf_tail(buf);
buf->len += tx_sdu_len;
for (int i = 0; i < frames_per_sdu; i++) {
int lc3_ret;
lc3_ret = lc3_encode(lc3_encoder, LC3_PCM_FORMAT_S16,
audio_buf, 1, octets_per_frame,
net_buffer + offset);
offset += octets_per_frame;
if (lc3_ret == -1) {
printk("LC3 encoder failed - wrong parameters?: %d",
lc3_ret);
net_buf_unref(buf);
return;
}
}
for (size_t i = 0U; i < configured_sink_stream_count; i++) {
struct bt_bap_stream *stream = &streams[i];
struct net_buf *buf_to_send;
int ret;
/* Clone the buffer if sending on more than 1 stream */
if (i == configured_sink_stream_count - 1) {
buf_to_send = buf;
} else {
buf_to_send = net_buf_clone(buf, K_FOREVER);
}
ret = bt_bap_stream_send(stream, buf_to_send, get_and_incr_seq_num(stream));
if (ret < 0) {
printk(" Failed to send LC3 audio data on streams[%zu] (%d)\n",
i, ret);
net_buf_unref(buf_to_send);
} else {
printk(" TX LC3 l on streams[%zu]: %zu\n",
tx_sdu_len, i);
sdu_cnt++;
}
}
}
}
static int init_lc3(void)
{
const struct bt_audio_codec_cfg *codec_cfg = &codec_configuration.codec_cfg;
unsigned int num_samples;
int ret;
ret = bt_audio_codec_cfg_get_freq(codec_cfg);
if (ret > 0) {
freq_hz = bt_audio_codec_cfg_freq_to_freq_hz(ret);
} else {
return ret;
}
ret = bt_audio_codec_cfg_get_frame_dur(codec_cfg);
if (ret > 0) {
frame_duration_us = bt_audio_codec_cfg_frame_dur_to_frame_dur_us(ret);
}
octets_per_frame = bt_audio_codec_cfg_get_octets_per_frame(codec_cfg);
frames_per_sdu = bt_audio_codec_cfg_get_frame_blocks_per_sdu(codec_cfg, true);
octets_per_frame = bt_audio_codec_cfg_get_octets_per_frame(codec_cfg);
if (freq_hz < 0) {
printk("Error: Codec frequency not set, cannot start codec.");
return -1;
}
if (frame_duration_us < 0) {
printk("Error: Frame duration not set, cannot start codec.");
return -1;
}
if (octets_per_frame < 0) {
printk("Error: Octets per frame not set, cannot start codec.");
return -1;
}
frame_duration_100us = frame_duration_us / 100;
/* Fill audio buffer with Sine wave only once and repeat encoding the same tone frame */
fill_audio_buf_sin(audio_buf, frame_duration_us, AUDIO_TONE_FREQUENCY_HZ, freq_hz);
num_samples = ((frame_duration_us * freq_hz) / USEC_PER_SEC);
for (unsigned int i = 0; i < num_samples; i++) {
printk("%3i: %6i\n", i, audio_buf[i]);
}
/* Create the encoder instance. This shall complete before stream_started() is called. */
lc3_encoder = lc3_setup_encoder(frame_duration_us,
freq_hz,
0, /* No resampling */
&lc3_encoder_mem);
if (lc3_encoder == NULL) {
printk("ERROR: Failed to setup LC3 encoder - wrong parameters?\n");
return -1;
}
return 0;
}
#else
#define init_lc3(...) 0
/**
* @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)
{
static uint8_t buf_data[CONFIG_BT_ISO_TX_MTU];
static bool data_initialized;
struct net_buf *buf;
static size_t len_to_send = 1;
if (!data_initialized) {
/* TODO: Actually encode some audio data */
for (int i = 0; i < ARRAY_SIZE(buf_data); i++) {
buf_data[i] = (uint8_t)i;
}
data_initialized = true;
}
buf = net_buf_alloc(&tx_pool, K_FOREVER);
net_buf_reserve(buf, BT_ISO_CHAN_SEND_RESERVE);
net_buf_add_mem(buf, buf_data, len_to_send);
/* 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 = 0U; i < configured_sink_stream_count; i++) {
struct bt_bap_stream *stream = &streams[i];
struct net_buf *buf_to_send;
int ret;
/* Clone the buffer if sending on more than 1 stream */
if (i == configured_sink_stream_count - 1) {
buf_to_send = buf;
} else {
buf_to_send = net_buf_clone(buf, K_FOREVER);
}
ret = bt_bap_stream_send(stream, buf_to_send, get_and_incr_seq_num(stream));
if (ret < 0) {
printk("Failed to send audio data on streams[%zu]: (%d)\n",
i, ret);
net_buf_unref(buf_to_send);
} else {
printk("Sending mock data with len %zu on streams[%zu]\n",
len_to_send, i);
}
}
k_work_schedule(&audio_send_work, K_USEC(AUDIO_DATA_TIMEOUT_US));
len_to_send++;
if (len_to_send > codec_configuration.qos.sdu) {
len_to_send = 1;
}
}
#endif
static void print_hex(const uint8_t *ptr, size_t len) static void print_hex(const uint8_t *ptr, size_t len)
{ {
while (len-- != 0) { while (len-- != 0) {
@ -519,6 +222,23 @@ static void stream_enabled(struct bt_bap_stream *stream)
k_sem_give(&sem_stream_enabled); k_sem_give(&sem_stream_enabled);
} }
static bool stream_is_tx(const struct bt_bap_stream *stream)
{
struct bt_bap_ep_info info;
int err;
if (stream == NULL || stream->ep == NULL) {
return false;
}
err = bt_bap_ep_get_info(stream->ep, &info);
if (err != 0) {
return false;
}
return info.can_send;
}
static void stream_connected_cb(struct bt_bap_stream *stream) static void stream_connected_cb(struct bt_bap_stream *stream)
{ {
printk("Audio Stream %p connected\n", stream); printk("Audio Stream %p connected\n", stream);
@ -537,6 +257,14 @@ static void stream_connected_cb(struct bt_bap_stream *stream)
static void stream_started(struct bt_bap_stream *stream) static void stream_started(struct bt_bap_stream *stream)
{ {
printk("Audio Stream %p started\n", stream); printk("Audio Stream %p started\n", stream);
/* Register the stream for TX if it can send */
if (IS_ENABLED(CONFIG_BT_AUDIO_TX) && stream_is_tx(stream)) {
const int err = stream_tx_register(stream);
if (err != 0) {
printk("Failed to register stream %p for TX: %d\n", stream, err);
}
}
k_sem_give(&sem_stream_started); k_sem_give(&sem_stream_started);
} }
@ -555,8 +283,14 @@ static void stream_stopped(struct bt_bap_stream *stream, uint8_t reason)
{ {
printk("Audio Stream %p stopped with reason 0x%02X\n", stream, reason); printk("Audio Stream %p stopped with reason 0x%02X\n", stream, reason);
/* Stop send timer */ /* Unregister the stream for TX if it can send */
k_work_cancel_delayable(&audio_send_work); if (IS_ENABLED(CONFIG_BT_AUDIO_TX) && stream_is_tx(stream)) {
const int err = stream_tx_unregister(stream);
if (err != 0) {
printk("Failed to unregister stream %p for TX: %d", stream, err);
}
}
} }
static void stream_released(struct bt_bap_stream *stream) static void stream_released(struct bt_bap_stream *stream)
@ -774,11 +508,9 @@ static int init(void)
bt_gatt_cb_register(&gatt_callbacks); bt_gatt_cb_register(&gatt_callbacks);
#if defined(CONFIG_LIBLC3) if (IS_ENABLED(CONFIG_BT_AUDIO_TX)) {
k_work_init_delayable(&audio_send_work, lc3_audio_timer_timeout); stream_tx_init();
#else }
k_work_init_delayable(&audio_send_work, audio_timer_timeout);
#endif
return 0; return 0;
} }
@ -999,14 +731,6 @@ static int set_stream_qos(void)
static int enable_streams(void) static int enable_streams(void)
{ {
if (IS_ENABLED(CONFIG_LIBLC3)) {
int err = init_lc3();
if (err != 0) {
return err;
}
}
for (size_t i = 0U; i < configured_stream_count; i++) { for (size_t i = 0U; i < configured_stream_count; i++) {
int err; int err;
@ -1200,11 +924,6 @@ int main(void)
} }
printk("Streams started\n"); printk("Streams started\n");
if (CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0) {
/* Start send timer */
k_work_schedule(&audio_send_work, K_MSEC(0));
}
/* Wait for disconnect */ /* Wait for disconnect */
err = k_sem_take(&sem_disconnected, K_FOREVER); err = k_sem_take(&sem_disconnected, K_FOREVER);
if (err != 0) { if (err != 0) {

View file

@ -0,0 +1,206 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <errno.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <zephyr/bluetooth/audio/audio.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/logging/log_core.h>
#include <zephyr/net/buf.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys_clock.h>
#include <lc3.h>
#include <sys/errno.h>
#include <math.h>
#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);
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)) {
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)) {
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;
}

View file

@ -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 <stdint.h>
#include <zephyr/bluetooth/audio/audio.h>
#include <zephyr/net/buf.h>
#include <zephyr/sys_clock.h>
/* 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 <lc3.h>
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 */

View file

@ -0,0 +1,176 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <autoconf.h>
#include <errno.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <zephyr/bluetooth/audio/cap.h>
#include <zephyr/bluetooth/audio/bap.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci_types.h>
#include <zephyr/bluetooth/iso.h>
#include <zephyr/kernel.h>
#include <zephyr/kernel/thread_stack.h>
#include <zephyr/logging/log.h>
#include <zephyr/logging/log_core.h>
#include <zephyr/net/buf.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/sys/util_macro.h>
#include <zephyr/types.h>
#include "stream_lc3.h"
#include "stream_tx.h"
LOG_MODULE_REGISTER(stream_tx, LOG_LEVEL_INF);
static struct tx_stream tx_streams[CONFIG_BT_BAP_UNICAST_CLIENT_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) {
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)) {
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 {
net_buf_add_mem(buf, mock_data, bap_stream->qos->sdu);
}
err = bt_bap_stream_send(bap_stream, buf, tx_streams[i].seq_num);
if (err == 0) {
tx_streams[i].seq_num++;
} 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);
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;
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef STREAM_TX_H
#define STREAM_TX_H
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/audio/bap.h>
#include <zephyr/bluetooth/audio/cap.h>
#include <zephyr/types.h>
#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 */

View file

@ -1,9 +1,6 @@
# For LC3 the following configs are needed # For LC3 the following configs are needed
CONFIG_FPU=y CONFIG_FPU=y
CONFIG_LIBLC3=y CONFIG_LIBLC3=y
# The LC3 codec uses a large amount of stack. This app runs the codec in the work-queue, hence
# inctease stack size for that thread.
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096
CONFIG_BT_BUF_EVT_RX_SIZE=255 CONFIG_BT_BUF_EVT_RX_SIZE=255
CONFIG_BT_BUF_ACL_RX_SIZE=255 CONFIG_BT_BUF_ACL_RX_SIZE=255

View file

@ -11,6 +11,9 @@ target_sources(app PRIVATE
${unicast_client_path}/src/main.c ${unicast_client_path}/src/main.c
) )
zephyr_sources_ifdef(CONFIG_LIBLC3 ${unicast_client_path}/src/stream_lc3.c)
zephyr_sources_ifdef(CONFIG_BT_AUDIO_TX ${unicast_client_path}/src/stream_tx.c)
target_sources(app PRIVATE target_sources(app PRIVATE
src/unicast_client_sample_test.c src/unicast_client_sample_test.c
src/test_main.c src/test_main.c

View file

@ -1,16 +1,2 @@
# This file content is just a copy of the unicast client sample prj.conf # Please build using the sample configuration file:
# ${ZEPHYR_BASE}/samples/bluetooth/uncast_audio_client/prj.conf
CONFIG_BT=y
CONFIG_LOG=y
CONFIG_BT_CENTRAL=y
CONFIG_BT_AUDIO=y
CONFIG_BT_BAP_UNICAST_CLIENT=y
CONFIG_BT_ISO_TX_BUF_COUNT=4
# Support an ISO channel per ASE
CONFIG_BT_ISO_MAX_CHAN=4
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=4
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT=2
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT=2
CONFIG_BT_KEYS_OVERWRITE_OLDEST=y
CONFIG_BT_EXT_ADV=y

View file

@ -14,7 +14,11 @@ source ${ZEPHYR_BASE}/tests/bsim/compile.source
if [ "${BOARD_TS}" == "nrf5340bsim_nrf5340_cpuapp" ]; then if [ "${BOARD_TS}" == "nrf5340bsim_nrf5340_cpuapp" ]; then
app=samples/bluetooth/bap_unicast_server sysbuild=1 compile app=samples/bluetooth/bap_unicast_server sysbuild=1 compile
app=samples/bluetooth/bap_broadcast_source sysbuild=1 compile app=samples/bluetooth/bap_broadcast_source sysbuild=1 compile
app=tests/bsim/bluetooth/audio_samples/bap_unicast_client sysbuild=1 compile app=tests/bsim/bluetooth/audio_samples/bap_unicast_client \
sample=${ZEPHYR_BASE}/samples/bluetooth/bap_unicast_client \
conf_file=${sample}/prj.conf \
conf_overlay=${sample}/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf \
exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile
app=tests/bsim/bluetooth/audio_samples/bap_broadcast_sink sysbuild=1 \ app=tests/bsim/bluetooth/audio_samples/bap_broadcast_sink sysbuild=1 \
conf_file=${ZEPHYR_BASE}/samples/bluetooth/bap_broadcast_sink/prj.conf \ conf_file=${ZEPHYR_BASE}/samples/bluetooth/bap_broadcast_sink/prj.conf \
exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile
@ -24,7 +28,9 @@ else
app=samples/bluetooth/bap_broadcast_source conf_overlay=overlay-bt_ll_sw_split.conf \ app=samples/bluetooth/bap_broadcast_source conf_overlay=overlay-bt_ll_sw_split.conf \
exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile
app=tests/bsim/bluetooth/audio_samples/bap_unicast_client \ app=tests/bsim/bluetooth/audio_samples/bap_unicast_client \
conf_overlay=${ZEPHYR_BASE}/samples/bluetooth/bap_unicast_client/overlay-bt_ll_sw_split.conf \ sample=${ZEPHYR_BASE}/samples/bluetooth/bap_unicast_client \
conf_file=${sample}/prj.conf \
conf_overlay=${sample}/overlay-bt_ll_sw_split.conf \
exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile
app=tests/bsim/bluetooth/audio_samples/bap_broadcast_sink \ app=tests/bsim/bluetooth/audio_samples/bap_broadcast_sink \
conf_file=${ZEPHYR_BASE}/samples/bluetooth/bap_broadcast_sink/prj.conf \ conf_file=${ZEPHYR_BASE}/samples/bluetooth/bap_broadcast_sink/prj.conf \