Bluetooth LE Audio tutorial – ISO Channels LC3 Codec Multi-Stream

Bluetooth LE Audio — Part 2
Bluetooth LE Audio tutorial – ISO Channels LC3 Codec Multi-Stream
LC3
New Universal Codec
CIS
Unicast Streams
BIS
Broadcast Streams
BT 5.2
Minimum Required

What You Will Learn

Part 1 showed why classic Bluetooth audio hit a wall. This part shows how LE Audio tears it down with a new transport, a better codec, and a topology that scales from one earbud to unlimited broadcast listeners.

  • The four core innovations in Bluetooth LE Audio
  • CIS — standard stereo earbuds without any proprietary chip
  • BIS — broadcast audio to unlimited receivers, no pairing needed
  • LC3 codec — better quality at lower bitrate than SBC
  • Linux kernel and BlueZ version requirements
  • Complete BlueZ ISO socket C code: CIS sender, CIS receiver, BIS source

The Four Core Innovations in LE Audio

Bluetooth LE Audio — Four Core Innovations
① LC3 Codec
One universal codec for both voice and music. Better quality than SBC at half the bitrate. Standardized — all devices use the same codec, no optional-codec fragmentation.
② ISO Channels
New isochronous transport over BLE. Fixed-interval delivery like SCO, but flexible. Eliminates every proprietary TWS relay and sniff scheme.
③ Multi-Stream
A phone maintains independent synchronized streams to left and right earbuds simultaneously. Standard feature — any BT 5.2 device, no proprietary chip required.
④ Broadcast Audio
One device broadcasts audio to unlimited receivers. No pairing, no connection. Replaces hearing loop (telecoil) systems in theatres, airports, and public spaces.

The LE Audio Protocol Stack

LE Audio replaces A2DP and HFP with layered specifications all running over BLE. Control uses GATT services (PACS, ASCS). Audio data uses the new ISO channel layer.

Bluetooth LE Audio — Full Protocol Stack
Audio Application — Music, Voice, Hearing Aid
VCP — Volume Control Profile
Standardized volume via GATT
CCP / MCP — Call / Media Control
Replaces HFP AT commands + AVRCP
PACS — Published Audio Capabilities
Advertises supported codecs (GATT)
ASCS — Audio Stream Control
Stream setup and control (GATT)
BAP — Basic Audio Profile
Defines unicast (CIS) and broadcast (BIS) streams
CIS — Connected Isochronous Stream
Unicast: earbuds, hearing aids
BIS — Broadcast Isochronous Stream
Broadcast: unlimited listeners
ISO Layer — Isochronous Channels (BT 5.2)
Fixed-interval guaranteed delivery over BLE link layer
BLE Link Layer — Bluetooth 5.2+
BLE Radio — 1M / 2M PHY

ISO Channels — How Guaranteed Timing Works

ISO channels deliver audio data with a fixed, repeating interval. Every device knows exactly when each packet will arrive. This predictability is what enables perfect multi-device synchronization without any relay or proprietary scheme.

ISO vs ACL — Fixed Interval vs Variable Timing
ISO Channel (LE Audio) — Fixed Interval ACL Channel (A2DP) — Variable Timing
← 10 ms →← 10 ms →← 10 ms →← 10 ms →
#1 #2 #3 #4 #5
Every receiver knows exactly when #N arrives
→ Perfect sync without relay
timing varies depending on retransmissions
#1 retry #2 #3 #4
Arrival time unpredictable
→ Sync between two earbuds requires proprietary hacks

CIS — Connected Isochronous Stream (Unicast Audio)

A CIS is an ISO channel between two connected BLE devices. For earbuds, the phone sets up two separate CIS connections — one to each earbud. Both receive their own channel directly from the phone. No relay. No sniffing. No proprietary chip.

Multiple CIS connections are organized into a CIG (Connected Isochronous Group). A CIG allows the phone to synchronize all streams to the same timing reference — left and right play in perfect sync, guaranteed by the standard.

CIG / CIS Hierarchy — Standard Stereo Earbud Setup

CIG — Connected Isochronous Group (ID = 0x01)
CIS ID = 0x01
LC3 LEFT channel
interval: 10 ms
SDU: 40 bytes
Same CIG →
synchronized
CIS ID = 0x02
LC3 RIGHT channel
interval: 10 ms
SDU: 40 bytes
🎧 Left Earbud
Receives directly from phone
No relay from right earbud
✅ Perfect sync
guaranteed by
CIG timing
🎧 Right Earbud
Receives directly from phone
No proprietary protocol
✅ With CIG/CIS (LE Audio) ❌ Without CIG/CIS (Classic TWS)
Standard — works with any BT 5.2 device
No relay, no forwarding
Mix earbuds from different brands
Proprietary chip required
Relay adds 10–30 ms extra latency
Cannot mix vendors

BIS — Broadcast Isochronous Stream (Broadcast Audio)

A BIS broadcasts audio to any number of listeners with no pairing and no connection required. Listeners just scan and tune in. This is the standard replacement for hearing loop (telecoil) systems used in theatres and public spaces.

BIG / BIS — One Source Broadcasting to Unlimited Receivers

BIG — Broadcast Isochronous Group (ID = 0x01)
BIS ID = 0x01 — LEFT channel (LC3) BIS ID = 0x02 — RIGHT channel (LC3)
📺 Broadcast Source
TV / Stage PA / Airport Gate
Sends BIG broadcast — no pairing needed
🦻 Hearing Aid
Listener 1
🎧 Earbuds
Listener 2
🎧 Headphones
Listener 3
🎧 Another
Listener 4

Unlimited
No pairing. No connection. No setup. Any BLE 5.2 device scans for the BIG → syncs to BIS streams → receives audio.

Real-world use cases: Airport gate announcements in your own hearing aid  |  Cinema audio track in your language  |  Gym silent disco  |  Museum guided tour audio

The LC3 Codec

LC3 (Low Complexity Communications Codec) is the mandatory codec for all Bluetooth LE Audio devices. Developed by Fraunhofer IIS and Ericsson. Unlike A2DP where codec fragmentation is a problem, every LE Audio device supports LC3 — no fallback negotiation needed.

LC3 vs SBC — Technical Comparison
Property SBC (A2DP mandatory) LC3 (LE Audio mandatory)
Bitrate range 128–345 kbps 16–320 kbps
Quality at 64 kbps Unusable Acceptable (telephone quality)
Quality at 128 kbps Basic, noticeable artifacts Better than SBC at 320 kbps
Frame duration ~7.5 ms fixed 7.5 ms or 10 ms (configurable)
Works for voice? No — need mSBC/CVSD for HFP Yes — same codec for voice + music
Fragmentation Optional codecs create mismatches Universal — all LE Audio devices use LC3
Sample rates 16, 32, 44.1, 48 kHz 8, 16, 24, 32, 44.1, 48 kHz

CIS vs BIS — Quick Reference

CIS (Unicast) vs BIS (Broadcast) — Key Differences
Property CIS — Connected Isochronous Stream BIS — Broadcast Isochronous Stream
Use case Earbuds, hearing aids, headsets Public audio, hearing loops, silent disco
Topology 1 source ↔ 1 sink (bidirectional) 1 source → unlimited sinks (unidirectional)
Connection needed? Yes — BLE connection first, then GATT (ASCS) No — scan → sync → receive
Pairing needed? Yes No
Group type CIG — Connected Isochronous Group BIG — Broadcast Isochronous Group
Linux socket connect() / accept() listen() / write()
QoS union member bt_iso_qos.ucast bt_iso_qos.bcast
Min kernel 5.10 5.19

Linux / BlueZ Requirements

Linux and BlueZ Version Requirements for LE Audio
Feature Min Kernel Min BlueZ Notes
BTPROTO_ISO socket 5.10 5.56+ Basic ISO socket API
CIS unicast audio 5.10 5.64+ Earbuds, hearing aids
BIS broadcast audio 5.19 5.66+ Broadcast source/sink
PACS + ASCS profiles Any 5.65+ GATT-based stream control via D-Bus
Hardware adapter Must support BT 5.2 + LE ISO feature bit

Code Example 1 — Check System Readiness

Before writing any LE Audio code, run this script to verify kernel version, BlueZ version, adapter ISO feature support, and that ISO sockets can actually be created.

#!/bin/bash
# check_le_audio.sh — Verify system readiness for LE Audio development
# Usage: chmod +x check_le_audio.sh && sudo ./check_le_audio.sh

echo "========================================="
echo "  Bluetooth LE Audio — System Check"
echo "========================================="

# ---- 1. Kernel version ----
KERNEL=$(uname -r)
KMAJ=$(echo "$KERNEL" | cut -d'.' -f1)
KMIN=$(echo "$KERNEL" | cut -d'.' -f2)
echo ""
echo "[1] Kernel: $KERNEL"

if [ "$KMAJ" -gt 5 ] || { [ "$KMAJ" -eq 5 ] && [ "$KMIN" -ge 19 ]; }; then
    echo "    OK  CIS (unicast) + BIS (broadcast) support: YES"
elif [ "$KMAJ" -eq 5 ] && [ "$KMIN" -ge 10 ]; then
    echo "    OK  CIS (unicast) support: YES"
    echo "    WARN BIS (broadcast): need kernel 5.19+"
else
    echo "    ERR  Need kernel 5.10+ for any LE Audio support"
fi

# ---- 2. BlueZ version ----
echo ""
echo "[2] BlueZ:"
if command -v bluetoothd &>/dev/null; then
    echo "    Version: $(bluetoothd --version 2>&1)"
    echo "    (Recommended: 5.65+ for full LE Audio profile support)"
else
    echo "    ERR  bluetoothd not found — install bluez package"
fi

# ---- 3. Adapter info ----
echo ""
echo "[3] Adapter info:"
if command -v hciconfig &>/dev/null; then
    hciconfig 2>/dev/null | grep -E "hci[0-9]|BD Address" | sed 's/^/    /'
fi

# ---- 4. LE ISO feature flag via btmgmt ----
echo ""
echo "[4] LE Isochronous Channels feature flag:"
if command -v btmgmt &>/dev/null; then
    FEAT=$(btmgmt info 2>/dev/null | grep -i "iso\|isochronous" | head -3)
    if [ -n "$FEAT" ]; then
        echo "$FEAT" | sed 's/^/    /'
    else
        echo "    (Not found — adapter may not support BT 5.2 LE ISO)"
    fi
fi

# ---- 5. BTPROTO_ISO socket test ----
echo ""
echo "[5] BTPROTO_ISO socket test:"
python3 - <<'PYEOF'
import socket
AF_BLUETOOTH  = 31
SOCK_SEQPACKET = 5
BTPROTO_ISO   = 6
try:
    sk = socket.socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_ISO)
    sk.close()
    print("    OK  BTPROTO_ISO socket creation supported")
except PermissionError:
    print("    WARN Run as root for ISO sockets (need CAP_NET_RAW)")
except OSError as e:
    print(f"    ERR  BTPROTO_ISO not available: {e}")
    print("         Need kernel 5.10+ for ISO socket support")
PYEOF

echo ""
echo "========================================="

Code Example 2 — CIS Sender (Phone / Source Side)

This is the core building block for LE Audio unicast development. BTPROTO_ISO replaces both BTPROTO_SCO (for classic voice) and BTPROTO_L2CAP (for A2DP) in the LE Audio world.

/* cis_sender.c — BlueZ ISO socket CIS unicast sender
 * Build: gcc cis_sender.c -o cis_sender -lbluetooth
 * Run:   sudo ./cis_sender AA:BB:CC:DD:EE:FF
 * Needs: kernel 5.10+, BlueZ 5.64+, BT 5.2 adapter
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/iso.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

#define CIG_ID  0x01
#define CIS_ID  0x01

/* Step 1 — Create the ISO socket
 * BTPROTO_ISO = 6 (defined in kernel bluetooth.h)
 * SOCK_SEQPACKET = ordered, packet-based delivery */
static int create_iso_socket(void)
{
    int sk = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_ISO);
    if (sk < 0) {
        perror("socket(BTPROTO_ISO)");
        fprintf(stderr, "Need: root, kernel 5.10+, BT 5.2 adapter\n");
        return -1;
    }
    printf("[1] ISO socket fd=%d\n", sk);
    return sk;
}

/* Step 2 — Bind to local adapter
 * BDADDR_ANY = let kernel select adapter */
static int bind_iso(int sk)
{
    struct sockaddr_iso src = { 0 };
    src.iso_family      = AF_BLUETOOTH;
    bacpy(&src.iso_bdaddr, BDADDR_ANY);
    src.iso_bdaddr_type = BDADDR_LE_PUBLIC;
    if (bind(sk, (struct sockaddr *)&src, sizeof(src)) < 0) {
        perror("bind"); return -1;
    }
    printf("[2] Bound to local adapter\n");
    return 0;
}

/* Step 3 — Set CIS QoS parameters
 *
 * interval: ISO packet interval in microseconds (10000 = 10ms)
 * latency:  max transport latency in milliseconds
 * sdu:      max SDU (Service Data Unit) size in bytes
 * phy:      BLE PHY — 0x01=1M, 0x02=2M, 0x03=Coded
 * rtn:      retransmission number
 *
 * LC3 at 16kHz mono 10ms frame → sdu = 40 bytes
 * LC3 at 48kHz mono 10ms frame → sdu = 120 bytes
 */
static int set_cis_qos(int sk)
{
    struct bt_iso_qos qos;
    memset(&qos, 0, sizeof(qos));

    qos.ucast.cig     = CIG_ID;
    qos.ucast.cis     = CIS_ID;
    qos.ucast.framing = 0x00;  /* unframed — fixed size SDUs */
    qos.ucast.packing = 0x00;  /* sequential */

    /* TX (outgoing audio to earbud) */
    qos.ucast.out.interval = 10000; /* 10ms in microseconds */
    qos.ucast.out.latency  = 10;    /* max 10ms transport latency */
    qos.ucast.out.sdu      = 40;    /* 40 bytes = LC3 16kHz mono 10ms */
    qos.ucast.out.phy      = 0x02;  /* 2M PHY */
    qos.ucast.out.rtn      = 2;     /* 2 retransmissions */

    /* RX (microphone audio back from earbud — bidirectional) */
    qos.ucast.in = qos.ucast.out;

    if (setsockopt(sk, SOL_BLUETOOTH, BT_ISO_QOS,
                   &qos, sizeof(qos)) < 0) {
        perror("setsockopt(BT_ISO_QOS)"); return -1;
    }
    printf("[3] QoS set: CIG=0x%02x CIS=0x%02x "
           "interval=%u latency=%u sdu=%u\n",
           CIG_ID, CIS_ID,
           qos.ucast.out.interval,
           qos.ucast.out.latency,
           qos.ucast.out.sdu);
    return 0;
}

/* Step 4 — Connect CIS to peer (earbud / hearing aid)
 * connect() sends HCI_LE_Create_CIG to controller,
 * which negotiates CIS with peer.
 * Blocks until CIS Established event received. */
static int connect_cis(int sk, const char *peer_str)
{
    struct sockaddr_iso dst = { 0 };
    bdaddr_t peer;
    str2ba(peer_str, &peer);
    dst.iso_family      = AF_BLUETOOTH;
    bacpy(&dst.iso_bdaddr, &peer);
    dst.iso_bdaddr_type = BDADDR_LE_PUBLIC;

    printf("[4] Connecting CIS to %s ...\n", peer_str);
    if (connect(sk, (struct sockaddr *)&dst, sizeof(dst)) < 0) {
        perror("connect(CIS)"); return -1;
    }
    printf("[4] CIS established\n");
    return 0;
}

/* Step 5 — Read back negotiated QoS
 * Controller may adjust parameters during negotiation.
 * Always read back to get actual agreed values. */
static uint16_t read_qos(int sk)
{
    struct bt_iso_qos qos;
    socklen_t len = sizeof(qos);
    if (getsockopt(sk, SOL_BLUETOOTH, BT_ISO_QOS, &qos, &len) < 0) {
        perror("getsockopt"); return 40;
    }
    printf("[5] Negotiated: interval=%u latency=%u sdu=%u phy=0x%02x\n",
           qos.ucast.out.interval, qos.ucast.out.latency,
           qos.ucast.out.sdu, qos.ucast.out.phy);
    return qos.ucast.out.sdu;
}

/* Step 6 — Send SDUs (one per ISO interval)
 * Each write() = one audio frame (one SDU).
 * In real code: fill buf with LC3-encoded PCM here. */
static void send_loop(int sk, uint16_t sdu)
{
    uint8_t *buf = calloc(1, sdu);
    if (!buf) { perror("calloc"); return; }

    printf("[6] Sending %u-byte SDUs (Ctrl+C to stop)\n", sdu);
    for (int n = 0; ; n++) {
        buf[0] = n & 0xFF;  /* dummy data — replace with LC3 frame */
        if (write(sk, buf, sdu) < 0) {
            if (errno == EINTR) break;
            perror("write"); break;
        }
        usleep(10000);  /* pace at 10ms ISO interval */
    }
    free(buf);
}

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <peer-addr>\n"
                        "Example: %s AA:BB:CC:DD:EE:FF\n",
                argv[0], argv[0]);
        return 1;
    }
    int sk = create_iso_socket(); if (sk < 0) return 1;
    if (bind_iso(sk) < 0)    goto done;
    if (set_cis_qos(sk) < 0) goto done;
    if (connect_cis(sk, argv[1]) < 0) goto done;
    send_loop(sk, read_qos(sk));
done:
    close(sk);
    return 0;
}

Code Example 3 — CIS Receiver (Earbud / Sink Side)

The sink side listens for incoming CIS connections using listen() and accept(), then reads SDUs (audio frames) in a loop.

/* cis_receiver.c — BlueZ ISO socket CIS receiver (sink side)
 * Build: gcc cis_receiver.c -o cis_receiver -lbluetooth
 * Run:   sudo ./cis_receiver
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/iso.h>

int main(void)
{
    int srv, cli;
    struct sockaddr_iso src = { 0 }, peer;
    socklen_t peer_len;
    uint8_t buf[255];
    ssize_t n;
    char addr[18];

    /* Create ISO socket */
    srv = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_ISO);
    if (srv < 0) { perror("socket"); return 1; }

    /* Bind to any local BLE adapter */
    src.iso_family      = AF_BLUETOOTH;
    bacpy(&src.iso_bdaddr, BDADDR_ANY);
    src.iso_bdaddr_type = BDADDR_LE_PUBLIC;
    if (bind(srv, (struct sockaddr *)&src, sizeof(src)) < 0) {
        perror("bind"); close(srv); return 1;
    }

    /* Listen — wait for incoming CIS from source */
    if (listen(srv, 1) < 0) {
        perror("listen"); close(srv); return 1;
    }
    printf("Waiting for incoming CIS connection...\n");

    /* Accept — blocks until CIS is established by remote */
    peer_len = sizeof(peer);
    cli = accept(srv, (struct sockaddr *)&peer, &peer_len);
    if (cli < 0) { perror("accept"); close(srv); return 1; }

    ba2str(&peer.iso_bdaddr, addr);
    printf("CIS connected from: %s\n", addr);

    /* Read SDUs in a loop
     * In production: pass each SDU to LC3 decoder,
     * then send decoded PCM to ALSA / PipeWire. */
    printf("Receiving audio frames (Ctrl+C to stop)...\n");
    for (int frame = 0; ; frame++) {
        n = read(cli, buf, sizeof(buf));
        if (n <= 0) {
            if (n == 0) printf("Remote disconnected\n");
            else if (errno != EINTR) perror("read");
            break;
        }
        /* For demo: print frame count and first byte */
        printf("Frame %4d  len=%zd  byte0=0x%02x\n",
               frame, n, buf[0]);
    }

    close(cli);
    close(srv);
    return 0;
}

Code Example 4 — bluetoothctl LE Audio Inspection

For quick testing without C code, bluetoothctl in BlueZ 5.65+ exposes LE Audio PACS/ASCS through its media menu.

$ bluetoothctl

# Scan with LE-only filter
[bluetooth]# power on
[bluetooth]# scan le
[NEW] Device AA:BB:CC:DD:EE:FF LE_Audio_Sink

# Connect
[bluetooth]# connect AA:BB:CC:DD:EE:FF
[CHG] Device AA:BB:CC:DD:EE:FF Connected: yes

# Check for LE Audio GATT services in device info
[bluetooth]# info AA:BB:CC:DD:EE:FF
  UUIDs:
    Published Audio Capabilities (0x184e)   <-- PACS present
    Audio Stream Control          (0x184e)   <-- ASCS present
    Volume Control                (0x1844)   <-- VCP present

# Enter media submenu (BlueZ 5.65+)
[bluetooth]# menu media

# List audio endpoints (from PACS)
[media]# list-endpoints
Endpoint /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/pac_source0
  UUID: Published Audio Capabilities
  Codec: LC3
  Capabilities: 16_2_1  (16kHz, 10ms frame, 40 bytes SDU)
  Capabilities: 48_4_1  (48kHz, 10ms frame, 120 bytes SDU)

# ---- Check adapter for BT 5.2 LE ISO feature ----
$ sudo btmgmt info
  Bluetooth Version: 12           # 12 = BT 5.3, 11 = BT 5.2
  Supported Features:
    ...
    LE Isochronous Channels       <-- must be present for LE Audio
    ...

# ---- Read LE Supported Features from HCI ----
$ sudo hcitool cmd 0x08 0x0003
# Response byte 3 bit 5 = LE Isochronous Channels (BT 5.2 feature)

Code Example 5 — BIS Broadcast Source

BIS uses the same BTPROTO_ISO socket but with the bcast QoS union member. The source calls listen() to start the BIG broadcast — no connection, no pairing. Requires kernel 5.19+.

/* bis_source.c — BIS broadcast audio source
 * Build: gcc bis_source.c -o bis_source -lbluetooth
 * Run:   sudo ./bis_source
 * Needs: kernel 5.19+, BT 5.2 adapter
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/iso.h>

/* LC3 48kHz / 10ms / mono preset: 48_4_1
 * SDU = 120 bytes, interval = 10ms */
#define BIG_ID      0x01
#define BIS_ID      0x01   /* one socket per BIS channel */
#define SDU_SIZE    120
#define ISO_US      10000  /* 10ms in microseconds */

int main(void)
{
    int sk;
    struct sockaddr_iso src = { 0 };
    struct bt_iso_qos qos;
    uint8_t frame[SDU_SIZE];

    /* Create ISO socket */
    sk = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_ISO);
    if (sk < 0) { perror("socket"); return 1; }

    /* Bind to any local adapter */
    src.iso_family      = AF_BLUETOOTH;
    bacpy(&src.iso_bdaddr, BDADDR_ANY);
    src.iso_bdaddr_type = BDADDR_LE_PUBLIC;
    if (bind(sk, (struct sockaddr *)&src, sizeof(src)) < 0) {
        perror("bind"); close(sk); return 1;
    }

    /* Configure BIS QoS — note: bcast union member, not ucast */
    memset(&qos, 0, sizeof(qos));
    qos.bcast.big        = BIG_ID;
    qos.bcast.bis        = BIS_ID;
    qos.bcast.packing    = 0x00;  /* sequential */
    qos.bcast.framing    = 0x00;  /* unframed */
    qos.bcast.encryption = 0x00;  /* no encryption */

    qos.bcast.out.interval = ISO_US;
    qos.bcast.out.latency  = 20;    /* 20ms max latency — broadcast needs more */
    qos.bcast.out.sdu      = SDU_SIZE;
    qos.bcast.out.phy      = 0x02;  /* 2M PHY */
    qos.bcast.out.rtn      = 4;     /* 4 retransmissions for broadcast */

    if (setsockopt(sk, SOL_BLUETOOTH, BT_ISO_QOS,
                   &qos, sizeof(qos)) < 0) {
        perror("setsockopt(BT_ISO_QOS bcast)");
        close(sk); return 1;
    }

    /* listen() starts the BIG in BLE Extended Advertising.
     * Any BLE 5.2+ scanner nearby can sync to this BIG
     * and receive BIS streams — no pairing, no connection. */
    if (listen(sk, 1) < 0) {
        perror("listen (BIG start)"); close(sk); return 1;
    }
    printf("BIG 0x%02x started — BIS 0x%02x broadcasting...\n",
           BIG_ID, BIS_ID);
    printf("Any BT 5.2+ device nearby can now receive this stream.\n");

    /* Send 1000 frames then exit */
    for (int i = 0; i < 1000; i++) {
        memset(frame, i & 0xFF, SDU_SIZE);  /* fill with dummy data */
        /* In production: fill frame with LC3-encoded PCM here */
        if (write(sk, frame, SDU_SIZE) < 0) {
            perror("write(BIS SDU)"); break;
        }
        usleep(ISO_US);
    }

    printf("Done. Closing BIG.\n");
    close(sk);
    return 0;
}

Key Takeaways — Part 2

LC3: one codec for voice AND music
ISO: fixed-interval guaranteed delivery over BLE
CIG/CIS: standard stereo earbuds, no proprietary chips
BIG/BIS: broadcast to unlimited listeners
BTPROTO_ISO: new socket type replacing SCO + ACL
bt_iso_qos.ucast: CIS QoS member
bt_iso_qos.bcast: BIS QoS member
Kernel 5.10+: CIS | Kernel 5.19+: BIS
PACS + ASCS: GATT services for stream control
BlueZ 5.65+: full LE Audio profile support

Next: PACS and ASCS — Stream Control in Depth

The next post dives into the GATT layer — PACS PAC records, ASCS ASE state machine, and how D-Bus interfaces in BlueZ expose LE Audio stream management to applications.

Leave a Reply

Your email address will not be published. Required fields are marked *