LC3 Deep Dive
2 — Sections 2.3 & 2.4
Beginner–Intermediate
In this tutorial, you will learn how an LC3 encode-transmit-decode session actually works end-to-end — including both fixed bitrate and variable bitrate operation. You will also learn the full set of decoder inputs and outputs and how the Bad Frame Indication (BFI) flag controls the decoder behavior.
2.3 LC3 High-Level Operation Description
How rate parameters flow from profile to link layer
LC3 sits between the Bluetooth audio profile (like BAP — Basic Audio Profile) and the Bluetooth LE link layer. The profile defines byte_count — the compressed frame size in bytes that the LC3 encoder shall produce. As long as byte_count is within the link layer’s maximum frame size, the payload is transmitted over the air.
On the receiver side, the LC3 decoder gets the received payload (payloadRX) plus the same byte_count — it needs to know how many bytes to expect for correct decoding. It also receives a BFI (Bad Frame Indication) flag. If BFI = 1, the payload has bit errors and the decoder should not attempt to decode it.
Case 1: Fixed Bitrate, Single Channel
This is the simplest case. The same byte_count is used for every single frame throughout the session. Both encoder and decoder use the same bits_per_audio_sample setting (though they are allowed to differ).
Encoder side (Transmitter):
- Receives InputPCM buffer of size
Nf × bits_per_audio_sample/8bytes. - Compresses to exactly
byte_countbytes →payloadTX. - Transmits over the Bluetooth LE ISO channel.
Decoder side (Receiver):
- Receives
payloadRXof sizebyte_countbytes. - If bit errors are detected → set
BFI = 1, otherwiseBFI = 0. - If BFI = 0: decode normally → produce
OutputPCM. - If BFI ≠ 0: skip decoding, use Packet Loss Concealment (PLC) to generate substitute audio.
Case 2: Variable Bitrate, Multiple Channels
LC3 also supports external rate control — where the profile can change byte_count per frame and per channel. This is used for things like dynamic bitrate adjustment without tearing down the stream.
Key rules in multi-channel variable bitrate mode:
| Item | Rule |
|---|---|
| byte_count[k] | Can be different per channel k and per frame. Range: 20–400 bytes. |
| Number of channels (Nc) | Fixed for the entire session. Does not change. |
| bits_per_audio_sample | Can differ between encoder (enc) and decoder (dec). Same bit depth for all channels. |
| BFI[k] | One flag per channel. Implementations should handle all channels jointly (mute all or none). |
| Total frame size | Sum of all channel byte_count values. Defined by the profile (e.g. BAP). |
Allowing different bits_per_audio_sample_enc and bits_per_audio_sample_dec means a decoder with only 16-bit capability can still decode a 24-bit or 32-bit encoded stream — it just outputs 16-bit samples.
The BFI Flag — How it Works
The Bad Frame Indication (BFI) flag is set externally by the Bluetooth link layer or the application — not by the LC3 decoder itself. It is a binary signal:
| BFI Value | Meaning | Decoder Action |
|---|---|---|
BFI = 0 |
No bit errors detected — payload is assumed correct | Decode payloadRX normally → output PCM |
BFI ≠ 0 |
Bit errors detected, or the LC3 internal bitstream is flagged as corrupt | Do NOT decode payloadRX. Apply PLC (Packet Loss Concealment) instead. |
LC3 also defines internal BEC (Bit Error Condition) fields within the bitstream itself. These allow the encoder to mark its own payload as corrupt, and the decoder can detect invalid bitstream states during parsing and trigger PLC automatically.
Note: The LC3 payload has no timestamps, no sequence numbers. Timing and ordering is handled entirely by the Bluetooth LE ISO transport.
2.4 Decoder Interfaces
Session Configuration Parameters (set once)
Like the encoder, the decoder requires a one-time session configuration. These must match the encoder except for bits_per_audio_sample_dec.
| Parameter | Allowed Values | Description |
|---|---|---|
{Fs, Nms, Nf} |
Same as encoder | Sampling rate, frame duration, frame size in samples. Must be identical to encoder session config. |
Nc |
1 to Nc_max | Number of audio channels. Fixed for the session. |
bits_per_audio_sample_dec |
16, 24, or 32 bits | Bit depth of the output PCM. May differ from encoder’s input bit depth. |
byte_count_max_dec |
Maximum of byte_count range | Maximum allowed byte_count per channel. Used to pre-allocate decoder buffers once — avoids runtime memory allocation during variable-bitrate operation. |
Per-Frame Inputs to the Decoder
| Input | Size / Range | Description |
|---|---|---|
BFI[Nc] |
0 or 1 per channel | Bad Frame Indication per channel. Set by the link layer or application. 0 = good frame, 1 = bad/corrupt frame. |
byte_count[Nc] |
20 to 400 bytes per channel | The size of the received payload for each channel. The decoder uses this to parse the bitstream correctly. |
payloadRX[Nc] |
byte_count[k] bytes per channel k | The received compressed audio payload for each channel. If BFI[k] ≠ 0, this data should not be decoded. |
Per-Frame Output from the Decoder
| Output | Size | Description |
|---|---|---|
OutputPCM[Nc] |
Nc × Nf × (bits_dec/8) bytes | The reconstructed PCM audio for all channels. Integer samples using bits_per_audio_sample_dec bits per sample. For a bad frame (BFI≠0), the output is the PLC substitute audio. |
BFI Handling in BlueZ LE Audio
When reading LC3 audio data from a BlueZ ISO socket, the kernel delivers an ancillary message indicating whether the received Bluetooth SDU had errors. This maps directly to the BFI flag that you pass to the LC3 decoder.
/* Reading from BlueZ ISO socket and extracting BFI from the cmsg */
/* The BT_SCM_PKT_STATUS ancillary message carries the packet status */
struct msghdr msg = {};
struct iovec iov;
struct cmsghdr *cmsg;
uint8_t payload[400]; /* max LC3 byte_count per channel */
uint8_t control[64];
uint8_t bfi = 0; /* default: good frame */
iov.iov_base = payload;
iov.iov_len = sizeof(payload);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
ssize_t len = recvmsg(iso_fd, &msg, 0);
if (len < 0) { perror("recvmsg"); return; }
/* Walk control messages to find packet status */
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_BLUETOOTH &&
cmsg->cmsg_type == BT_SCM_PKT_STATUS) {
uint8_t pkt_status = *(uint8_t *)CMSG_DATA(cmsg);
/* 0x00 = valid, 0x01 = possibly invalid, 0x02 = no data */
bfi = (pkt_status != 0x00) ? 1 : 0;
}
}
/* Now pass payload and bfi to the LC3 decoder */
lc3_decode(decoder, payload, (int)len, LC3_PCM_FORMAT_S16,
pcm_output, /* stride */ 1);
/* For a real implementation you would check bfi first:
if (bfi) { run_plc(decoder, pcm_output); }
else { lc3_decode(decoder, payload, len, ...); }
*/
Quick Summary
| Concept | Key Point |
|---|---|
| Fixed bitrate | byte_count is the same every frame. Simplest mode. |
| Variable bitrate | byte_count can change per frame and per channel. Profile-driven. |
| BFI = 0 | Good frame. Decode payloadRX normally. |
| BFI ≠ 0 | Bad frame. Do not decode. Apply PLC. |
| Decoder session config | Same as encoder except bits_per_audio_sample_dec can differ. |
| Decoder output | OutputPCM: Nc × Nf × (bits_dec/8) bytes of reconstructed audio. |
Next in this Series
Chapter 3 begins: General Codec Description — math symbols, operators, and the feature summary table
