bluetooth le audio tutorial – Bluetooth Public Broadcast Profile (PBP)
The profile behind Auracast™ — how LE Audio broadcasts reach hearing aids, earbuds, and speakers in airports, gyms, and theatres
Topics Covered:
PBP Public Broadcast Profile LE Audio Auracast BIG Extended Advertising BAP CAP BlueZ Broadcast Audio Broadcast_Name LTV Metadata
The Problem PBP Solves
Picture an airport lounge with ten TV screens. With classic Bluetooth, your earbuds pair to exactly one device at a time — you cannot listen to the departure gate screen without pairing to it first. Bluetooth LE Audio fixes this with broadcast audio: one source transmits, unlimited receivers tune in, no pairing required. Just like FM radio, but digital and lossless.
But a gap remained. How does a receiver — say a hearing aid — quickly figure out what kind of audio is being broadcast? Without extra help, it has to synchronize to the source’s periodic advertisements and parse a complex data structure called the BASE (Broadcast Audio Source Endpoint). That process takes time and drains battery.
PBP fills that gap. It defines a compact announcement that piggybacks on the extended advertisement itself. A receiver can instantly read: “Standard Quality audio, unencrypted, broadcast name is Gate 3.” No sync step needed just to make that decision.
🔌 Where PBP Lives in the LE Audio Stack
PBP is a thin profile that sits on top of CAP (Common Audio Profile), which itself is built on BAP (Basic Audio Profile). PBP does not invent a new audio transport — it only defines the announcement format that a broadcaster uses to say “I am a public broadcast source and here is what I offer.” It extends only the broadcast part of CAP; unicast (connected) audio is out of scope.
LE Audio Profile Stack — PBP Position
Public Broadcast Profile (PBP)
Common Audio Profile (CAP)
Basic Audio Profile (BAP)
Bluetooth Core Spec 5.2+ (LE)
PBP extends only the broadcast portion of CAP. No unicast procedures.
🎪 The Three PBP Roles
Every device in a PBP deployment plays one or more of these roles. A single device can implement multiple roles at the same time — for example, a smartphone commonly acts as both a PBA (controlling a hearing aid) and a PBS (streaming audio from its microphone).
PBP Roles and Typical Devices
PBS
Public Broadcast Source
📺 TV / Monitor
💻 PC / Laptop
🎤 Microphone
📱 Smartphone
CAP Initiator + BAP Broadcast Source
→
PBK
Public Broadcast Sink
🧒 Hearing Aid
🎧 Earbuds
🎧 Headphones
🔊 Speaker
CAP Acceptor + BAP Broadcast Sink
+
PBA
Public Broadcast Assistant
📱 Smartphone
⌛ Smartwatch
📺 Smart TV
CAP Commander + BAP Broadcast Assistant
PBA is optional — it helps a PBK choose and receive the right broadcast without the sink scanning independently
The PBA role deserves special attention. Think of it as a remote control for PBK devices. Your phone (PBA) does the scanning work, discovers nearby PBP sources, then pushes the selected stream’s details to your hearing aid (PBK) over GATT. The hearing aid then syncs directly to the BIG — saving it from burning battery on scanning.
PBP → CAP → BAP Role Mapping
| PBP Role |
CAP Role (M) |
BAP Broadcast Role (M) |
BAP Scan Delegator |
| PBS (Source) |
Initiator |
Broadcast Source |
— |
| PBK (Sink) |
Acceptor |
Broadcast Sink |
Mandatory¹ |
| PBA (Assistant) |
Commander |
Broadcast Assistant |
Optional¹ |
¹ Requirement inherited from BAP. Every PBP implementation must support at least one of the three roles.
📄 The Public Broadcast Announcement — Byte by Byte
This is the core of PBP. When a PBS transmits extended advertisements (AUX_ADV_IND PDUs), it includes a Public Broadcast Announcement inside the Service Data AD type. Here is what that structure looks like at the byte level.
Public Broadcast Announcement — AD Payload Structure
| Field |
Bytes |
Example Value |
Notes |
| AD Length |
1 |
0x05 |
Length of Type + Value that follow |
| AD Type |
1 |
0x16 |
Service Data — 16-bit UUID |
| Service UUID |
2 |
0x56 0x18 |
UUID 0x1856 in little-endian (PBP identifier) |
| Features |
1 |
0x02 |
Bitfield: bit0=Encrypted, bit1=SQ, bit2=HQ, bits3–7=RFU |
| Metadata_Length |
1 |
0x00 |
0x00 means no metadata present |
| Metadata |
Varies |
— |
LTV-formatted. Present only when Metadata_Length ≠ 0x00 |
Minimum announcement (no metadata) = 6 bytes total. Very compact for an extended advertising payload.
Features Bitfield Explained
The single Features byte is the key differentiator of PBP. A scanner reads this byte and immediately knows three things about the broadcast.
Features Byte — Bit Breakdown (1 byte = 8 bits)
| bit0 = 1 |
🔒 BIG is encrypted. Receiver needs a Broadcast_Code (16-byte key) to decode. |
| bit1 = 1 |
🎵 Standard Quality audio is present. Any BAP-compliant sink can decode it. |
| bit2 = 1 |
🎧 High Quality audio (48 kHz) is present. Requires a capable sink. |
bit1 and bit2 are NOT mutually exclusive — a broadcaster can offer both SQ and HQ streams inside the same BIG simultaneously.
🎵 Standard Quality vs High Quality Audio
Standard Quality (SQ) — bit1
Codec configurations marked Mandatory for BAP Broadcast Sinks (BAP Table 6.4). Any compliant receiver can handle this without knowing anything more about the stream.
✓ Works on every BAP Broadcast Sink — maximum interoperability
High Quality (HQ) — bit2
Any of the 48 kHz sampling rate configurations from BAP Table 6.4 — specifically the twelve 48_x_x settings. Better audio fidelity for music and high-quality speech.
★ Higher fidelity — receiver must confirm HQ capability first
HQ Configurations — All Valid BAP Config Names
48_1_1 48_2_1 48_3_1 48_4_1 48_5_1 48_6_1 48_1_2 48_2_2 48_3_2 48_4_2 48_5_2 48_6_2
Format: SampleRate_FrameDuration_RetransmitSet | Blue = reliability set 1 | Green = set 2 (higher retransmission, better link robustness)
📖 Advertising Data Structures — Broadcast_Name and LTV Metadata
Broadcast_Name AD Type (0x30)
PBP mandates that the PBS transmit both the Public Broadcast Announcement and the Broadcast_Name in the same AUX_ADV_IND PDU. Think of the Broadcast_Name as the “Wi-Fi SSID” of a broadcast stream — the human-readable label shown on a user’s phone or hearing aid when listing nearby broadcasts.
Broadcast_Name AD Type — Format and Rules
| Rule |
Value |
Example |
| AD Type |
0x30 |
Defined in Bluetooth Assigned Numbers |
| Encoding |
UTF-8 |
Supports international characters |
| Min length |
4 characters |
“Gate” |
| Max length |
32 characters |
“Auracast_Room:2A” |
| Shared names |
Allowed |
Two transmitters covering the same hall can both use “Hall-A” — scanners treat them as redundant sources for the same content |
LTV Metadata Structures
Three LTV structures are defined by PBP. Their byte values live in Bluetooth Assigned Numbers, not in the PBP spec itself. They can appear either in the Public Broadcast Announcement metadata field or inside the BASE structure (periodic advertisement).
PBP LTV Metadata Structures at a Glance
| LTV Structure Name |
What It Does |
Where Used |
| Broadcast_Name |
LTV form of the Broadcast_Name string. Lets a PBA write the name to a PBK’s GATT characteristic so the sink knows the stream’s label without re-scanning. |
PBP Announcement metadata |
| Audio_Active_State |
Signals whether the stream currently carries live audio or is silent. Sinks can skip synchronizing to a silent stream and save power. |
PBP Announcement or BASE subgroup |
| Broadcast_Audio_Immediate_Rendering_Flag |
Tells sinks to render audio at the earliest possible moment rather than waiting for the Presentation_Delay window. Ideal for PA systems and live announcements where latency matters. |
PBP Announcement or BASE |
Note: Sinks that are part of a Coordinated Set (e.g. left+right hearing aids) should only act on the Immediate Rendering Flag if all set members can render simultaneously.
🔎 PBP Discovery Flow — How It Works End to End
Here is the step-by-step sequence showing how a PBK discovers and receives a broadcast using PBP announcements.
PBP Discovery and Reception Sequence
PBS
(TV / Source)
1. Starts extended adv
AUX_ADV_IND with:
• PBP Service Data (UUID 0x1856 + features)
• Broadcast_Name AD type
3. Broadcasts BASE data on AUX_SYNC_IND
(codec configs, subgroups)
5. Transmits BIG with ISO audio packets continuously
PBK
(Earbud / Hearing Aid)
2. Scans. Reads PBP announcement instantly:
“SQ audio, not encrypted, name = Gate 3”
✓ Decides to receive — no sync needed yet
4. Syncs to periodic adv. Reads BASE to get exact codec config details
6. Syncs to BIG — receives, decodes, and plays audio 🎵
⚡ PBP advantage: Step 2 lets the sink decide whether to proceed before the expensive periodic sync in step 4. This is the entire point of the profile.
🔧 BlueZ Implementation — Building the PBP Advertisement
Now the hands-on part. Below are two practical code examples — a C header for building the raw AD payload bytes, and a Python script using BlueZ’s D-Bus API to register the advertisement with the BlueZ daemon.
Part 1 — C Header: Building the PBP AD Data
This header constructs the exact byte sequence that goes into the extended advertising payload. Tested on BlueZ 5.66 / Ubuntu 22.04 with a Bluetooth 5.2+ adapter.
/* ======================================================= * pbp_adv.h — PBP Announcement AD Builder * EmbeddedPathashala | BlueZ LE Audio example * Compiler: gcc -std=c11 * ======================================================= */ #ifndef PBP_ADV_H #define PBP_ADV_H #include <stdint.h> #include <string.h> /* Public Broadcast Announcement UUID = 0x1856 (little-endian on wire) */ #define PBP_UUID_LSB 0x56u #define PBP_UUID_MSB 0x18u /* AD type codes */ #define AD_TYPE_SERVICE_DATA 0x16u /* Service Data — 16-bit UUID */ #define AD_TYPE_BROADCAST_NAME 0x30u /* Broadcast_Name (PBP-defined) */ /* Feature bitfield masks */ #define PBP_FEAT_ENCRYPTED (1u << 0) /* bit0: BIG is encrypted */ #define PBP_FEAT_SQ (1u << 1) /* bit1: Standard Quality present */ #define PBP_FEAT_HQ (1u << 2) /* bit2: High Quality present */ /* * pbp_build_announcement() * ————————- * Writes the 6-byte PBP Service Data AD entry into buf[]. * * Wire layout produced: * [0] Length = 0x05 (Type + 4 value bytes) * [1] AD Type = 0x16 (Service Data, 16-bit UUID) * [2] UUID LSB = 0x56 * [3] UUID MSB = 0x18 * [4] features = caller-supplied bitfield * [5] Metadata_Len = 0x00 (no inline metadata) * * Returns: bytes written (always 6) * Requires: buf must be at least 6 bytes */ static inline int pbp_build_announcement(uint8_t *buf, uint8_t features) { buf[0] = 0x05u; /* AD Length (does not count itself) */ buf[1] = AD_TYPE_SERVICE_DATA; buf[2] = PBP_UUID_LSB; buf[3] = PBP_UUID_MSB; buf[4] = features; /* e.g. PBP_FEAT_SQ = 0x02 */ buf[5] = 0x00u; /* Metadata_Length = 0 */ return 6; } /* * pbp_append_name() * —————— * Appends the Broadcast_Name AD entry after the announcement bytes. * * name : UTF-8 string, must be 4..32 characters * * Wire layout produced (for name “Gate 3”, 6 chars): * [0] Length = 0x07 (AD type + 6 name bytes) * [1] AD Type = 0x30 (Broadcast_Name) * [2..7] UTF-8 bytes of the name * * Returns: bytes written, or 0 on invalid name length */ static inline int pbp_append_name(uint8_t *buf, const char *name) { size_t n = strlen(name); if (n < 4u || n > 32u) return 0; buf[0] = (uint8_t)(n + 1u); /* Length byte */ buf[1] = AD_TYPE_BROADCAST_NAME; memcpy(&buf[2], name, n); return (int)(n + 2u); } #endif /* PBP_ADV_H */
Part 2 — main.c: Using the Header
/* ======================================================= * main.c — PBP payload construction demo * * Compile: gcc -o pbp_demo main.c * Run: ./pbp_demo * ======================================================= */ #include <stdio.h> #include <stdint.h> #include <string.h> #include “pbp_adv.h” static void hexdump(const char *label, const uint8_t *buf, int len) { printf(“%-26s (%2d bytes): “, label, len); for (int i = 0; i < len; i++) printf(“%02X “, buf[i]); printf(“\n”); } int main(void) { uint8_t adv[31]; /* extended adv payload ceiling per PDU */ int pos = 0; memset(adv, 0, sizeof(adv)); /* Build: SQ audio, unencrypted broadcast */ pos += pbp_build_announcement(adv + pos, PBP_FEAT_SQ); hexdump(“PBP Announcement”, adv, pos); /* Append broadcast name */ pos += pbp_append_name(adv + pos, “EmbeddedPathashala”); hexdump(“Full AD payload”, adv, pos); /* * Expected output: * * PBP Announcement ( 6 bytes): 05 16 56 18 02 00 * Full AD payload (26 bytes): 05 16 56 18 02 00 * 14 30 45 6D 62 65 64 64 65 64 * 50 61 74 68 61 73 68 61 6C 61 * * Features 0x02 breakdown: * bit0 (Encrypted) = 0 –> open broadcast * bit1 (SQ) = 1 –> Standard Quality present * bit2 (HQ) = 0 –> no HQ stream * * To transmit, pass adv[] to: * hci_le_set_extended_advertising_data() (direct HCI) * or BlueZ D-Bus Advertisement1.ServiceData (see Part 3) */ uint8_t f = adv[4]; printf(“\nFeature byte 0x%02X:\n”, f); printf(” Encrypted : %s\n”, (f & PBP_FEAT_ENCRYPTED) ? “YES” : “no”); printf(” SQ Audio : %s\n”, (f & PBP_FEAT_SQ) ? “YES” : “no”); printf(” HQ Audio : %s\n”, (f & PBP_FEAT_HQ) ? “YES” : “no”); return 0; }
Part 3 — Python: Register via BlueZ D-Bus
BlueZ exposes a LEAdvertisement1 D-Bus interface that lets you register an advertisement with the BlueZ daemon. This approach works on BlueZ 5.62+ with LE Audio extensions compiled in. Run as root.
#!/usr/bin/env python3 “”” pbp_dbus_adv.py — PBP broadcaster via BlueZ D-Bus EmbeddedPathashala | BlueZ LE Audio example Requirements: sudo apt install python3-dbus python3-gi Run as root: sudo python3 pbp_dbus_adv.py Verify with nRF Connect app (scan for UUID 0x1856) “”” import dbus import dbus.service import dbus.mainloop.glib from gi.repository import GLib BLUEZ_SVC = “org.bluez” LE_MGR_IFACE = “org.bluez.LEAdvertisingManager1” LE_ADV_IFACE = “org.bluez.LEAdvertisement1” PROPS_IFACE = “org.freedesktop.DBus.Properties” OBJ_MGR_IFACE = “org.freedesktop.DBus.ObjectManager” # 128-bit expanded form of UUID 0x1856 (Public Broadcast Announcement) PBP_UUID = “00001856-0000-1000-8000-00805f9b34fb” # Feature byte: bit1 = SQ present, bit0 = not encrypted PBP_FEAT_SQ = 0x02 class PBPSource(dbus.service.Object): “”” Implements org.bluez.LEAdvertisement1. BlueZ reads GetAll() to build the extended advertising PDU. “”” PATH = “/org/embeddedpathashala/pbp_src0″ def __init__(self, bus): super().__init__(bus, self.PATH) @dbus.service.method(PROPS_IFACE, in_signature=”s”, out_signature=”a{sv}”) def GetAll(self, iface): if iface != LE_ADV_IFACE: raise dbus.exceptions.InvalidArgsException() # Service data value bytes placed after the UUID: # features(1) + Metadata_Length(1) = 2 bytes # BlueZ prepends the UUID automatically when building the AD entry. svc_payload = dbus.Array( [dbus.Byte(PBP_FEAT_SQ), dbus.Byte(0x00)], signature=”y” ) return { # “broadcast” = non-connectable extended advertising (LE Periodic) “Type”: dbus.String(“broadcast”), # The PBP UUID causes BlueZ to include this as Service Data “ServiceUUIDs”: dbus.Array([PBP_UUID], signature=”s”), # Attach features + metadata_len as the service data payload “ServiceData”: dbus.Dictionary( {PBP_UUID: svc_payload}, signature=”sv” ), # Broadcast_Name AD type — the human-readable stream label “LocalName”: dbus.String(“EP_Live_Demo”), # 0 = advertise indefinitely until UnregisterAdvertisement “Duration”: dbus.UInt16(0), } @dbus.service.method(LE_ADV_IFACE) def Release(self): print(“[PBP] Released by BlueZ daemon”) def find_adapter(bus): mgr = dbus.Interface( bus.get_object(BLUEZ_SVC, “/”), OBJ_MGR_IFACE ) for path, ifaces in mgr.GetManagedObjects().items(): if “org.bluez.Adapter1” in ifaces: return path raise RuntimeError(“No Bluetooth adapter found. Is bluetoothd running?”) def main(): dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus() src = PBPSource(bus) adapter = find_adapter(bus) mgr = dbus.Interface( bus.get_object(BLUEZ_SVC, adapter), LE_MGR_IFACE ) def ok(): print(f”[PBP] Registered on {adapter}”) print(“[PBP] Broadcasting: SQ audio | unencrypted | name=EP_Live_Demo”) print(“[PBP] Scan with nRF Connect > Extended Scanner > filter UUID 0x1856”) print(“[PBP] Ctrl+C to stop”) def err(e): print(f”[PBP] Registration error: {e}”) mgr.RegisterAdvertisement(src.PATH, {}, reply_handler=ok, error_handler=err) try: GLib.MainLoop().run() except KeyboardInterrupt: print(“\n[PBP] Stopping…”) mgr.UnregisterAdvertisement(src.PATH) if __name__ == “__main__”: main()
Part 4 — Quick Verification with BlueZ CLI Tools
## — Step 1: Check adapter supports extended advertising — hciconfig hci0 features ## — Step 2: Start packet capture in one terminal — sudo btmon | grep -A5 “Extended Advertising” ## — Step 3: Run the Python script in another terminal — sudo python3 pbp_dbus_adv.py ## — Step 4: Confirm the UUID is visible via bluetoothctl — bluetoothctl [bluetooth]# scan on ## Look for devices advertising UUID 0x1856 in the scan output ## — Step 5: Decode the service data field manually — ## Expected: 56 18 02 00 ## 56 18 = UUID 0x1856 in little-endian ## 02 = Features: bit1=1 (SQ present), bit0=0 (not encrypted) ## 00 = Metadata_Length = 0
After running the script, open nRF Connect for Mobile, go to the Scanner tab, switch to Extended Scanner mode, and filter by UUID 0x1856. You should see “EP_Live_Demo” listed with the feature byte value confirming Standard Quality unencrypted audio.
🔒 Security — Encrypted vs Unencrypted BIG
🔓 Bit 0 = 0 (Unencrypted)
Any PBK can receive without any code. Used for genuinely public broadcasts — airport departure announcements, museum audio guides, gym background music, public transport PA systems.
🔒 Bit 0 = 1 (Encrypted)
The BIG uses a 16-byte Broadcast_Code. Important constraint: all streams in a BIG share the same key — you cannot encrypt some BISes and leave others open. Either the whole BIG is encrypted or none of it is.
PBP adds no security requirements beyond what BAP already defines. The Broadcast_Code itself is distributed out-of-band (for example, displayed as a QR code at the venue entrance).
💡 Key Takeaways
📢 PBP = fast broadcast discovery via extended adv
🏠 UUID 0x1856 identifies a PBP source
📄 Core payload is only 6 bytes minimum
🎪 Three roles: PBS, PBK, PBA
🎵 SQ = mandatory BAP configs (all sinks)
🎧 HQ = 48 kHz configs (capable sinks)
🔒 Encryption applies to whole BIG, one key
📱 PBA offloads scanning from battery-sensitive sinks
🔌 Sits on top of CAP → BAP → BT Core 5.2
🔧 BlueZ D-Bus LEAdvertisement1 for implementation
More LE Audio on EmbeddedPathashala
PBP is one piece of the LE Audio ecosystem. Pair it with BAP for stream management, CAP for multi-device coordination, and TMAP for telephony and media use cases.
prev_tutorial
next_tutorial