BLE LE Audio: Isochronous Streams Explained

BLE LE Audio: Isochronous Streams Explained
CIS, BIS, NSE, FT, BN — all the hard stuff made simple, with real BlueZ code
Chapter 4
Core Concepts
CIS + BIS
Both Stream Types
BlueZ ISO
Real Code Examples
15 Sections
Step by Step

Why Do We Need Isochronous Streams?

When you stream music from your phone to wireless earbuds, the audio must arrive at exactly the right time. A small delay in regular data transfer (like downloading a file) doesn’t matter. But for audio, even a 20ms inconsistency causes clicks, pops, or the left and right earbuds going out of sync.

Traditional BLE uses ACL links — these are great for data but not designed for strict timing. Bluetooth LE Audio solves this with Isochronous Streams: time-scheduled channels where audio packets go out at fixed, predictable intervals — like a train that runs exactly on time, every single time.

This post covers everything from the basics of CIS and BIS, through robustness parameters, all the way to BlueZ ISO socket code you can actually run.

Key Terms in This Post

ISO Interval Anchor Point Subevent CIS BIS CIG BIG Initiator Acceptor NSE Flush Timeout Burst Number CIE bit BIGInfo BASE ISOAL PAST bt_iso_qos

1. Two Types of Isochronous Streams: CIS and BIS

There are two flavours of Isochronous Streams in BLE Audio. Choosing which one to use depends on your use case:

CIS vs BIS — Complete Comparison
Feature CIS — Connected Isochronous Stream BIS — Broadcast Isochronous Stream
Connection needed? Yes — needs an ACL link first No — fully connectionless
Acknowledgements? Yes — receiver confirms each packet No — fire and forget
Direction Bidirectional possible (mic + speaker) One-way only (source → sinks)
Who can receive? Only the paired device Any device in range
Typical use case Your earbuds, phone calls, gaming headsets Public PA, hearing loops, TV audio sharing
Group container CIG — Connected Isochronous Group BIG — Broadcast Isochronous Group
Range Limited by the weak return ACK path Much greater — no ACK power penalty
BlueZ socket call connect() to earbud address connect() to BDADDR_ANY

Everyday analogy: CIS is like a phone call — both sides confirm the conversation. BIS is like an FM radio station — it just transmits and anyone with a radio can listen.

Interesting real-world case: When you share music with friends, your phone app can silently switch from CIS (private stream to your earbuds) to BIS (encrypted broadcast) so all your friends’ earbuds can tune in — without you doing anything. The switch happens at the protocol layer, invisible to the user.

2. Roles: Initiator and Acceptor

BLE Audio has a strict hierarchy. One device is always in charge of timing and scheduling — everything else follows its lead.

Initiator vs Acceptor — Who Does What
Initiator (Central)

Creates and schedules every audio packet’s timing

Sends LE_Set_CIG_Parameters HCI command
Sends LE_Create_CIS to start streaming
Manages radio sharing with Wi-Fi

Usually: phone, tablet, smart TV
Needs bigger battery and CPU

In Core spec = Central
In CAP spec = Initiator role

Acceptor (Peripheral)

Follows the Initiator’s schedule

Receives HCI LE CIS Request event
Replies with HCI LE Accept CIS Request
Goes to sleep between transmissions

Usually: earbuds, hearing aids, speakers
Optimised for low power

In Core spec = Peripheral
In CAP spec = Acceptor role

Important thing to remember: Either device can be the audio source or sink. Your earbud’s microphone sends voice data back to the phone — the earbud is still the Acceptor, but it generates audio data. The Initiator tells it exactly when to send that data.

As we go up the protocol stack, the names change:

Role Names at Different Protocol Layers
Layer Initiator Side Acceptor Side
Core (Link Layer) Central Peripheral
BAPS / GATT Client Server
CAP Initiator Acceptor
Top-level profiles Sender / Broadcaster Receiver / Broadcast Sink

3. CIS Timing — ISO Interval and Anchor Points

The LC3 codec (mandatory for BLE Audio) works in 10ms frames — it grabs 10ms of audio, compresses it, and hands it off to Bluetooth. Bluetooth transmits it in a fixed window called the Isochronous Interval.

CIS Timeline — How Audio Packets Flow
← ISO Interval 10ms → ← ISO Interval 10ms → ← ISO Interval 10ms →
D — Audio Packet Ack ✓ D — Audio Packet Ack ✓ D — Audio Packet ✗ Lost

Anchor
Point 1

Anchor
Point 2
Initiator → ← Acceptor No Ack received

The 3rd packet gets no Ack — Initiator knows it was lost and can decide to retransmit

Term What it means
ISO Interval Fixed time gap between each audio transmission. Usually 10ms. Like the tick of a metronome.
Anchor Point The exact moment each ISO Interval begins — the starting “tick”. Determined by the Initiator.
D (Data) The audio packet — contains one encoded LC3 frame (10ms of audio compressed to ~100 bytes)
Ack Acknowledgement from the Acceptor: “I got the packet, it was valid”

4. Subevents — Retransmissions and Frequency Hopping

Wireless environments have interference. What if one transmission gets blocked by Wi-Fi? BLE Audio’s answer: retry the same packet multiple times within one ISO Interval, each time on a different radio channel. Each retry slot is called a Subevent.

One CIS Event = 4 Subevents, Each on a Different Frequency
← CIS Event (entire ISO Interval) →
Sub_Interval 1 Sub_Interval 2 Sub_Interval 3 Sub_Interval 4
Tx Rx Tx Rx Tx Rx Tx Rx
Subevent 1
Channel f₁
Subevent 2
Channel f₂
Subevent 3
Channel f₃
Subevent 4
Channel f₄

If Wi-Fi blocks f₁, the same audio packet retries on f₂, f₃, f₄ — at least one channel should be clear

The inter-frame gap between Subevents is always 150 microseconds — just enough time for the radio to switch channels. The BLE Core 5.2 spec introduced an improved channel selection algorithm that decides which frequency to use for each Subevent.

The CIE Bit — Saving Battery

Once the Acceptor successfully receives the packet and sends an Ack, the Initiator sets the CIE (Close Isochronous Event) bit in its next transmission. This tells the Acceptor: “you already got the packet — go to sleep until the next ISO Interval.” No need to stay awake for Subevents 3 and 4.

CIE Bit — Early Event Close = Battery Saving
Tx (Data) Rx (Ack ✓) Tx (CIE=1, NPI=1) Subevent 3 FREE Subevent 4 FREE
Packet delivered Close early Freed for Wi-Fi or other BT

The freed Subevents can be used by the chip for Wi-Fi transmissions or other Bluetooth connections — very important since phones share one antenna for both Wi-Fi and Bluetooth.

5. The CIS PDU — What’s Inside Each Audio Packet

Every packet transmitted over the air has this overall structure:

BLE Link Layer Packet (air interface)
Preamble
1-2 bytes
Access Address
4 bytes
ISO PDU (Header + Payload + Optional MIC)
2 – 257 bytes
CRC
3 bytes

The ISO PDU’s header contains control bits that manage the flow of data. For a CIS, these are:

CIS PDU Header — 16 bits total
LLID
2 bits
NESN
1 bit
SN
1 bit
CIE
1 bit
RFU
1 bit
NPI
1 bit
RFU
1 bit
Length
8 bits
Field What it stands for Simple explanation
LLID Link Layer ID Is this packet framed or unframed? (framing = how audio frames map to PDUs)
NESN Next Expected Sequence Number The receiver tells the sender which packet it’s expecting next — this is how ACKs work
SN Sequence Number The sequence number of this current packet being sent
CIE Close Isochronous Event Set to 1 means: “packet received, closing this event early, Acceptor can sleep now”
NPI Null Payload Indicator Set to 1 means: “this packet has no audio data” — just used for timing or ACK purposes

6. Robustness Parameters: NSE, Flush Timeout, and Burst Number

These three parameters control how hard the system tries to get each audio packet through. You do not set these directly — you give the Controller higher-level hints (latency, SDU size) and it calculates the best NSE/FT/BN values automatically.

NSE, FT, BN — The Robustness Triangle
Name Full Name What it does Effect on latency
NSE Number of Subevents Maximum retransmission attempts per ISO Interval (like number of tries per round) No direct effect
FT Flush Timeout How many consecutive ISO Intervals a packet can try to get through before it’s abandoned (flushed) ↑ FT = ↑ Latency
BN Burst Number How many audio packets are bundled into one CIS event (requires a bigger ISO Interval) ↑ BN = ↑ ISO Interval

NSE=4, FT=1 Example — Four Tries Per Interval

NSE=4, FT=1 — Packet P0 Fails All 4 Times → Flushed. P1 Succeeds on 2nd Try
ISO Interval 1 — Flush Point FP0 ISO Interval 2 — Flush Point FP1 ISO Interval 3
P0 ✗ P0 ✗ P0 ✗ P0 ✗
FLUSHED
P1 ✓ P1
ACK→CIE
P2 ✓ P2
ACK→CIE

P0 was lost (bad interference). The Acceptor reconstructs it using Packet Loss Correction. P1 and P2 get through fine.

FT=3 — More Intervals = More Chances (But More Latency)

With FT=3, the packet P0 can retry across 3 ISO Intervals (30ms total) before being flushed. This helps in noisy environments but adds up to 30ms of latency. The risk with FT alone is that a struggling packet (P0) can hog all Subevents, blocking P1 and P2 from getting airtime.

BN=2 — Fairness Fix (Share Subevents Between Two Packets)

The solution to the “hogging” problem is Burst Number. With BN=2, the ISO Interval is doubled to 20ms, so two packets (P0 and P1) are loaded into each interval. The 4 Subevents are shared between them — P0 can’t consume all 4. This is fairer but doubles the interval length.

💡 Key fact: Your application code sets Maximum Transport Latency, Max SDU size, and SDU interval via HCI. The Bluetooth Controller chip takes these inputs and decides NSE, FT, and BN internally. You can’t override them directly — and that’s intentional, so apps don’t break each other’s radio scheduling.

7. Bidirectional CIS — Voice Calls Over BLE Audio

For a phone call you need audio flowing both ways at the same time. BLE Audio is clever about this: instead of creating a second CIS, the Acceptor’s acknowledgement packet carries the microphone audio back to the Initiator. One CIS, two audio streams.

Bidirectional CIS — Left Earbud Has Mic, Right Earbud Doesn’t
Initiator
(Phone)
L (audio→) Ack (CIE) R (audio→) Ack (CIE) L (audio→) Ack (CIE)
Left Earbud
(has mic)
←Mic Data
inside Ack
←Mic Data
inside Ack
Right Earbud
(no mic)
Ack only
NPI=1

The microphone data from the left earbud travels inside its acknowledgement packet back to the phone. The right earbud has no microphone, so it sends a plain acknowledgement with NPI=1 (null payload, no audio data).

Rules for bidirectional CIS (important for implementation):

  • Only the Initiator sets the CIE bit — and only after both directions are acknowledged
  • FT and BN can differ for each direction (uplink voice vs downlink music may have different needs)
  • ISO Interval and NSE must be the same for both directions
  • If the Initiator has had its packet ACKed but hasn’t received the Acceptor’s data yet, it must keep sending null PDUs to give the Acceptor chances to retransmit

8. Multiple CIS Streams — CIG and Left/Right Sync

When your phone streams audio to both earbuds simultaneously, it creates two CIS streams inside one CIG. These can be scheduled in two ways:

Sequential vs Interleaved CIS Arrangements (NSE=3)
Sequential — CIS 0 completes first

CIS 0 (Left) CIS 1 (Right)
se1 se2 se3 se1 se2 se3

Gap between CIS 0 end and CIS 1 start wastes airtime if connection is reliable

vs
Interleaved — CIS 0 and CIS 1 alternate

C0 se1 C1 se1 C0 se2 C1 se2 C0 se3 C1 se3

If both earbuds ACK their first Subevent, both close early → large gap freed for Wi-Fi

How Left and Right Earbuds Stay in Sync

Left and right earbuds don’t talk to each other — but they must play audio at precisely the same moment. The solution is the CIG synchronisation point and CIS Sync Delays:

CIG Synchronisation — Each Earbud Knows Its Own Delay
CIG
Reference
Point
CIS 0 Event
(Left Earbud)
CIS 1 Event
(Right Earbud)
CIG
Sync
Point
← CIS_Sync_Delay[0] → ← CIS_Sync_Delay[1] →

Both earbuds calculate the same CIG Sync Point → add Presentation Delay → render audio at the same microsecond

The Presentation Delay (set by the Basic Audio Profile, BAP) is an extra buffer added after the Sync Point. Both earbuds wait exactly this long before playing — even if one received its packet earlier than the other. This is why BLE Audio earbuds achieve microsecond-level synchronisation without knowing each other exists.

9. Setting Up a CIG — State Machine and HCI Commands

The Bluetooth Host (your app code, via BlueZ) sends HCI commands to the Controller chip to set up streams. The CIG goes through these states:

CIG State Machine
No CIG
Nothing configured
→ LE_Set_CIG_Parameters → Configurable CIG
CISes defined, not active
0-31 CIS configs
→ LE_Create_CIS → Active CIG
Audio flowing!
1+ CIS active

From Active, disconnecting all CISes → Inactive CIG. Use LE_Remove_CIG to return to No CIG.

HCI Command What it tells the Controller
LE_Set_CIG_Parameters Define the CIG and all CISes: SDU size, max latency, PHY, retransmission count hint
LE_Create_CIS Activate one or more of the configured CISes → actual audio streaming begins
LE_Remove_CIG Tear down the entire CIG completely

10. BlueZ Code — CIS with ISO Sockets

BlueZ 5.64+ exposes BLE Audio via ISO sockets. They look and feel like regular BSD sockets, but under the hood they trigger all the HCI commands we just discussed. Here’s a step-by-step guide:

Step 1 — Create the ISO Socket

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/iso.h>    /* ISO socket support — BlueZ 5.64+ */

int main(void)
{
    int sock_fd;

    /*
     * PF_BLUETOOTH = Bluetooth address family
     * SOCK_SEQPACKET = message-oriented, ordered, each send() = one SDU
     * BTPROTO_ISO   = ISO transport protocol (new in BlueZ 5.64)
     */
    sock_fd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_ISO);
    if (sock_fd < 0) {
        perror("socket() failed - check BlueZ version >= 5.64");
        return -1;
    }
    printf("ISO socket created, fd=%d\n", sock_fd);
    return 0;
}

Step 2 — Bind to Local Adapter

/* sockaddr_iso is the BLE Audio equivalent of sockaddr_in */
struct sockaddr_iso local_addr;
memset(&local_addr, 0, sizeof(local_addr));

local_addr.iso_family      = AF_BLUETOOTH;
local_addr.iso_bdaddr      = *BDADDR_ANY;        /* Use default HCI adapter */
local_addr.iso_bdaddr_type = BDADDR_LE_PUBLIC;

if (bind(sock_fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {
    perror("bind() failed");
    close(sock_fd);
    return -1;
}
printf("Bound to local BLE adapter\n");

Step 3 — Set QoS Parameters

This is where you specify latency and SDU size. BlueZ passes these to the Controller via LE_Set_CIG_Parameters — the Controller then calculates NSE, FT, and BN internally.

struct bt_iso_qos qos;
memset(&qos, 0, sizeof(qos));

/*
 * BT_ISO_QOS_CIG_UNSET and BT_ISO_QOS_CIS_UNSET mean:
 * "let the Controller auto-assign IDs"
 */
qos.ucast.cig = BT_ISO_QOS_CIG_UNSET;
qos.ucast.cis = BT_ISO_QOS_CIS_UNSET;

/* ----- OUTGOING: phone -> earbud (music/audio) ----- */
qos.ucast.out.interval = 10000;  /* SDU interval in microseconds (= 10ms) */
qos.ucast.out.latency  = 10;     /* Maximum transport latency in ms */
qos.ucast.out.sdu      = 100;    /* Maximum SDU size in bytes (LC3 frame) */
qos.ucast.out.phy      = 0x02;   /* PHY: 0x01=1M, 0x02=2M (preferred for audio) */
qos.ucast.out.rtn      = 2;      /* Retransmission number hint (Controller decides FT/NSE) */

/* ----- INCOMING: earbud mic -> phone (voice) ----- */
/* Set sdu=0 for unidirectional (music-only, no mic) */
qos.ucast.in.interval  = 10000;
qos.ucast.in.latency   = 10;
qos.ucast.in.sdu       = 0;      /* 0 = no uplink audio (unidirectional) */
qos.ucast.in.phy       = 0x02;
qos.ucast.in.rtn       = 2;

if (setsockopt(sock_fd, SOL_BLUETOOTH, BT_ISO_QOS, &qos, sizeof(qos)) < 0) {
    perror("setsockopt(BT_ISO_QOS) failed");
    return -1;
}
printf("QoS set: 10ms interval, 10ms latency, 100-byte SDU, 2M PHY\n");

Step 4 — Connect to the Earbud (Triggers LE_Create_CIS)

bdaddr_t earbud_bdaddr;
/* Replace with your earbud's Bluetooth address */
str2ba("AA:BB:CC:DD:EE:FF", &earbud_bdaddr);

struct sockaddr_iso remote_addr;
memset(&remote_addr, 0, sizeof(remote_addr));
remote_addr.iso_family      = AF_BLUETOOTH;
remote_addr.iso_bdaddr      = earbud_bdaddr;
remote_addr.iso_bdaddr_type = BDADDR_LE_PUBLIC;

/*
 * connect() triggers:
 *   1. LE_Set_CIG_Parameters HCI command (Controller calculates NSE/FT/BN)
 *   2. LE_Create_CIS HCI command (opens the audio stream)
 *   3. Earbud receives HCI LE CIS Request and accepts it
 *   4. Both sides get HCI LE CIS Established event
 */
if (connect(sock_fd, (struct sockaddr *)&remote_addr, sizeof(remote_addr)) < 0) {
    perror("connect() failed - is device paired and in range?");
    return -1;
}
printf("CIS established! Audio stream is live.\n");

Step 5 — Send Audio SDUs (10ms loop)

uint8_t audio_sdu[100];  /* One 10ms LC3-encoded audio frame */

/*
 * In production: replace with LC3 encoder output.
 * Each send() delivers exactly one SDU (one ISO Interval of audio).
 * BlueZ handles fragmenting it into PDUs, NSE retransmissions, etc.
 */
while (1) {
    /* TODO: fill audio_sdu with actual LC3-encoded audio */
    memset(audio_sdu, 0x55, sizeof(audio_sdu)); /* silence placeholder */

    ssize_t written = send(sock_fd, audio_sdu, sizeof(audio_sdu), 0);
    if (written < 0) {
        perror("send() failed");
        break;
    }

    /* Sleep for one ISO Interval (10ms = 10000 microseconds) */
    usleep(10000);
}

close(sock_fd);
return 0;
✅ What BlueZ handles automatically: CIG creation, CIG parameter negotiation, NSE/FT/BN calculation, Subevent scheduling, frequency hopping, CIE bit management, ISOAL fragmentation. You just call send() every 10ms with your audio data.

11. Broadcast Isochronous Streams (BIS) — No ACKs, Anyone Can Listen

BIS is the broadcast version. The transmitter just sends continuously — no pairing, no acknowledgements, no per-device overhead. Any device within range can tune in.

BIS PDU Header — Simpler Than CIS (No ACK fields needed)
LLID
2 bits
CSSN
3 bits
CSTF
1 bit
RFU
2 bits
Length
8 bits

No NESN, SN, CIE, or NPI bits — because there are no acknowledgements in broadcast

Field Full Name What it does
CSSN Control Subevent Sequence Number Version number for control information — receiver uses this to detect if the control info has changed
CSTF Control Subevent Transmission Flag Set to 1 means “a Control Subevent follows after the data Subevents” (used for channel map updates)

BIS Robustness — Group Count, IRC, and PTO

Since there are no ACKs, BIS uses different tricks to ensure delivery:

BN=3, NSE=6 → Group Count=2, Each Packet Sent Twice
BIS Event — NSE=6 Subevents
P0 P1 P2 P0 P1 P2
Group 0 (g=0) Group 1 (g=1)
BN=3, NSE=6 → GC=2 → each of 3 packets sent twice on different channels
💡 PTO (Pre-Transmission Offset) explained simply: Normally a packet for Event X is only sent during Event X. With PTO, the broadcaster also sends it early during Event X-1. If Event X gets hit by a burst of interference (Wi-Fi channel scan, microwave oven), the receiver already got the packet one interval earlier. More robust, at the cost of slightly higher latency.

12. How Devices Find a Broadcast Stream — Extended Advertising

With no connection, how does an earbud know where to tune in? The broadcaster publishes a multi-hop chain of advertising packets:

Extended Advertising Chain — Step by Step Discovery
1. ADV_EXT_IND
Primary ch 37/38/39

Broadcaster’s address (AdvA)
Set ID (ADI)
Pointer to next: AuxPtr →

2. AUX_ADV_IND
Secondary ch 0-36

Broadcast Audio UUID
Broadcast_ID (3 bytes)
Pointer to PA: SyncInfo →

3. AUX_SYNC_IND
Periodic Advertising

ACAD: BIGInfo (timing)
AdvData: BASE (audio content)
BIG_Offset (where to find BIS)

4. BIS Audio Data
37 data channels

Actual encoded audio packets
(now that device knows timing)

What’s in BIGInfo?

BIGInfo (inside the ACAD field) gives the receiver everything it needs to decode the timing and structure of the BIG. Key fields include ISO_Interval, Sub_Interval, BIS_Spacing, NSE, BN, PTO, IRC, Max_PDU, Channel Map, and PHY. For encrypted broadcasts, it also contains GIV and GSKD (encryption keys derivation info).

What’s in BASE? The 3-Level Structure

BASE Structure — What Audio Content Is Available
Level 1 (BIG-wide) — Presentation Delay + Number of Subgroups
Level 2: Subgroup 0
LC3 Codec Config + Metadata
“Programme Info: English News”
Level 2: Subgroup 1
LC3 Codec Config + Metadata
“Programme Info: Spanish News”
Level 3: BIS[0x00]
Channel: Left
Level 3: BIS[0x01]
Channel: Right
Level 3: BIS[0x02]
Channel: Left
Level 3: BIS[0x03]
Channel: Right

A left earbud reads BIS[0x00], a right earbud reads BIS[0x01]. Headphones read both. A hearing loop in Spanish reads BIS[0x02] and [0x03].

13. BlueZ Code — BIG Broadcaster and BIS Sink

Setting up a BIG in BlueZ is simpler than CIS because there’s no per-device pairing. You bind to broadcast addresses and start transmitting. BlueZ handles the Extended Advertising chain automatically.

Broadcaster — Create a BIG and Start Sending Audio

#include <bluetooth/bluetooth.h>
#include <bluetooth/iso.h>
#include <unistd.h>

int main(void)
{
    int bc_fd;
    bc_fd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_ISO);

    /* Bind to local adapter */
    struct sockaddr_iso local = {
        .iso_family      = AF_BLUETOOTH,
        .iso_bdaddr      = *BDADDR_ANY,
        .iso_bdaddr_type = BDADDR_LE_PUBLIC,
    };
    bind(bc_fd, (struct sockaddr *)&local, sizeof(local));

    /* Configure BIG QoS */
    struct bt_iso_qos qos;
    memset(&qos, 0, sizeof(qos));

    qos.bcast.big      = BT_ISO_QOS_BIG_UNSET;  /* Auto-assign BIG ID */
    qos.bcast.bis      = BT_ISO_QOS_BIS_UNSET;  /* Auto-assign BIS ID */
    qos.bcast.packing  = 0x00;  /* Sequential packing of BISes */
    qos.bcast.framing  = 0x00;  /* Unframed (10ms codec = 10ms ISO Interval) */
    qos.bcast.encryption = 0x00; /* No encryption — public broadcast */

    /* Audio stream parameters */
    qos.bcast.out.interval = 10000;  /* 10ms */
    qos.bcast.out.latency  = 10;
    qos.bcast.out.sdu      = 100;
    qos.bcast.out.phy      = 0x02;   /* 2M PHY */
    qos.bcast.out.rtn      = 4;      /* More retransmissions — no ACKs in BIS */

    setsockopt(bc_fd, SOL_BLUETOOTH, BT_ISO_QOS, &qos, sizeof(qos));

    /*
     * connect() to BDADDR_ANY = start broadcasting
     * BlueZ automatically sets up:
     *   - ADV_EXT_IND on primary channels 37/38/39
     *   - AUX_ADV_IND on secondary channels with Broadcast_ID
     *   - Periodic Advertising train with BIGInfo and BASE
     *   - Then starts the BIG itself
     */
    struct sockaddr_iso dst = {
        .iso_family      = AF_BLUETOOTH,
        .iso_bdaddr      = *BDADDR_ANY,
        .iso_bdaddr_type = BDADDR_LE_PUBLIC,
    };
    if (connect(bc_fd, (struct sockaddr *)&dst, sizeof(dst)) < 0) {
        perror("broadcast connect() failed");
        return -1;
    }
    printf("BIG started! Broadcasting audio on 10ms intervals...\n");

    /* Send audio every 10ms */
    uint8_t audio_buf[100];
    while (1) {
        /* In real code: fill with LC3-encoded audio */
        memset(audio_buf, 0xAB, sizeof(audio_buf));
        send(bc_fd, audio_buf, sizeof(audio_buf), 0);
        usleep(10000); /* 10ms = 10000 microseconds */
    }

    close(bc_fd);
    return 0;
}

Broadcast Sink — Sync to a BIG and Receive Audio

int sink_fd;
sink_fd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_ISO);

/*
 * To sync to a specific broadcaster:
 * 1. Scan to find the ADV_EXT_IND and get the broadcaster's address
 * 2. Read AUX_ADV_IND to get the Broadcast_ID
 * 3. Sync to the Periodic Advertising train to get BIGInfo and BASE
 * 4. Use the BIS_Index from BASE to choose which BIS to receive
 * BlueZ tools like btmon and btvirt can help with testing
 */

/* Broadcaster's BT address (from scanning AUX_ADV_IND) */
bdaddr_t bc_addr;
str2ba("11:22:33:44:55:66", &bc_addr); /* broadcaster's address */

struct sockaddr_iso sink_addr = {
    .iso_family      = AF_BLUETOOTH,
    .iso_bdaddr      = bc_addr,
    .iso_bdaddr_type = BDADDR_LE_PUBLIC,
    .iso_bc_sid      = 0x00,  /* SID from ADV_EXT_IND */
};

/* Configure sync timeout */
struct bt_iso_qos sink_qos;
memset(&sink_qos, 0, sizeof(sink_qos));
sink_qos.bcast.sync_timeout = 0x4000; /* sync timeout in 10ms units */
sink_qos.bcast.mse          = 0x00;   /* max subevents to scan */

setsockopt(sink_fd, SOL_BLUETOOTH, BT_ISO_QOS, &sink_qos, sizeof(sink_qos));

/*
 * connect() = start syncing to the BIG
 * BlueZ reads the Periodic Advertising, parses BIGInfo,
 * and starts receiving audio packets
 */
if (connect(sink_fd, (struct sockaddr *)&sink_addr, sizeof(sink_addr)) < 0) {
    perror("sync to BIG failed - is broadcaster advertising?");
    return -1;
}
printf("Synced to BIG! Receiving audio...\n");

/* Receive audio SDUs */
uint8_t recv_buf[256];
while (1) {
    ssize_t len = recv(sink_fd, recv_buf, sizeof(recv_buf), 0);
    if (len > 0) {
        /* Each recv() gives one complete SDU = one 10ms audio frame */
        printf("Received %zd bytes of audio\n", len);
        /* In real code: pass to LC3 decoder, then to audio output */
    }
}

close(sink_fd);
✅ Testing tip: Use btmon to see all HCI commands and events. BlueZ’s tools/isotest.c in the BlueZ source tree is an excellent reference — it shows both CIS and BIS examples with full error handling.

14. ISOAL — The Isochronous Adaptation Layer

ISOAL solves a very specific problem: what if the codec frame length and the ISO Interval don’t match? This is handled entirely inside the Bluetooth chip — you never write ISOAL code — but understanding it helps when debugging audio glitches.

When is ISOAL Needed?
Unframed (no ISOAL work)

Codec frame = 10ms
ISO Interval = 10ms

One complete audio frame fits
neatly in one PDU

LLID bits indicate: unframed

This is the normal BLE Audio case

Framed (ISOAL handles it)

Codec frame = 7.5ms
ISO Interval = 10ms

Frames don’t divide evenly —
must be split across PDUs

LLID bits indicate: framed

Needed for dual Classic+LE devices

The 7.5ms mismatch occurs because classic Bluetooth devices (old mice, keyboards) use a 7.5ms radio interval. A dual-mode device (phone) that supports both Classic and LE Audio must align its timing — ISOAL handles the conversion transparently.

ISOAL Position in the Stack
Your Application / LC3 Codec (SDUs)
ISO Adaptation Manager (ISOAL)
Fragmentation & Recombination
(Unframed PDUs)
Segmentation & Reassembly
(Framed PDUs with Time_Offset)
Link Layer (PDUs, Subevents, Frequency Hopping)

In the framed case, each SDU segment gets a Time_Offset field which tells the receiver exactly when the original SDU’s audio should be played relative to the Anchor Point — so even though the audio data is split across multiple PDUs, the receiver reassembles it and plays it at exactly the right time.

15. Quick Reference — Every Term at a Glance
Abbreviation Full Name What it means in plain English
CIS Connected Isochronous Stream Private audio stream with acknowledgements — your earbuds
BIS Broadcast Isochronous Stream Public one-way audio stream — like FM radio over Bluetooth
CIG Connected Isochronous Group Container holding all CISes for one session (e.g., left + right earbud)
BIG Broadcast Isochronous Group Container holding all BISes for one broadcast (e.g., English left/right + Spanish left/right)
ISO Interval Isochronous Interval Fixed timing gap between audio transmissions — typically 10ms
Anchor Point The precise start moment of each ISO Interval — like the tick of a metronome
NSE Number of Subevents Maximum retry attempts per ISO Interval (each on a different frequency channel)
FT Flush Timeout Max ISO Intervals before giving up on a packet and discarding it
BN Burst Number Packets per CIS/BIS event — larger BN = bigger ISO Interval
CIE Close Isochronous Event Header bit that says “stop retransmitting, packet delivered, Acceptor can sleep”
NPI Null Payload Indicator Header bit that says “this packet carries no audio data — just used for timing”
IRC Immediate Repetition Count Number of groups in a BIS event that carry current (not future) data
PTO Pre-Transmission Offset BIS only: send future packets early in current event for extra robustness
ISOAL Isochronous Adaptation Layer Chip-internal layer that handles frame-size mismatch between codec and Link Layer
BASE Broadcast Audio Source Endpoint Describes what each BIS contains: codec, language, left/right channel allocation
BIGInfo BIG Information Timing structure of a BIG broadcast in Periodic Advertising (ISO_Interval, BIS_Spacing, channel map etc.)
PAST Periodic Advertising Sync Transfer Phone tells your earbuds where to find the BIG — earbuds skip scanning and save battery
bt_iso_qos BlueZ ISO QoS struct The C struct you pass to setsockopt() to configure audio stream parameters in BlueZ

Keep Building on BLE Audio

You now understand the foundation of BLE Audio’s Isochronous Streams. Next steps: BAP (Basic Audio Profile), ASCS (Audio Stream Control Service), the LC3 codec in depth, and hands-on BlueZ pairing + GATT setup.

EmbeddedPathashala Home More Bluetooth Posts

Leave a Reply

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