Bluetooth LE Audio — Top Level Profiles
HAP · TMAP · PBP — Roles, QoS, and BlueZ Integration
What Are Top-Level Profiles?
The Generic Audio Framework (GAF) lays the groundwork — BAP, PACS, ASCS, CCP, MCP. Top-level profiles sit above all of that. They do not define new procedures; they pick specific combinations of GAF features and lock them down for a particular use case. Think of them as product recipes. A manufacturer declaring TMAP support must implement an agreed set of codec configs and roles, so any TMAP device can talk to any other TMAP device without guessing what the other side supports.
This post covers the three profiles that shipped first: HAP (Hearing Access), TMAP (Telephony & Media), and PBP (Public Broadcast).
Key Terms in This Post
HAP / HAS TMAP / TMAS PBP LC3 Codec QoS Settings Unicast / Broadcast CG / CT Roles UMS / UMR Roles BMS / BMR Roles Hearing Aid Presets Presentation Delay BAP Codec Config BlueZ LE Audio Extended Advertisement
Part 1 — HAP: Hearing Access Profile
The entire BLE Audio specification effort started because the hearing aid industry pushed for it. Hearing aids are not earbuds. They run all day, they capture ambient sound continuously, and replacing a battery mid-day is a serious inconvenience for the wearer. Those constraints directly shape what HAP mandates and what it deliberately leaves optional.
Why Hearing Aids Are Different
A regular earbud user can tolerate a bit of audio latency — the eardrum only hears the Bluetooth stream. A hearing aid wearer hears both ambient sound and the Bluetooth stream simultaneously. If the stream is delayed by even 30–40ms relative to the ambient sound, the user perceives an echo. That is why Low Latency QoS is strongly preferred in HAP, and why the profile mandates a maximum Presentation Delay of 20ms for HA-role devices in Low Latency mode.
On the codec side, HAP does not push quality higher than BAP mandates. It uses 16_2 (16kHz, 7kHz BW) for voice and 24_2 (24kHz, 11kHz BW) for music. The trade-off is battery: higher sampling rates draw more power, and a hearing aid user who cannot hear while charging has a much bigger problem than an earbud user.
HAP — Four Physical Hearing Aid Configurations
🎧
Single HA
One device.
Renders one BLE Audio stream.
Mono output.
1 Stream → 1 Ear
🎧
Dual-Stream HA
Single device receives separate Left + Right streams, mixes them internally.
L+R Streams → 1 Device
🎧🎧
Binaural Set
Two HAs in a Coordinated Set (CSIS Size=2). Each handles one ear independently.
L Stream→HA-L · R Stream→HA-R
📡
Banded HA
BT transceiver in a neckband/headband. Wired connection routes audio to each ear.
BT → Neckband → Wired L+R
HAP Roles
HAP Role Map — Who Does What
| Role |
Full Name |
Acts As |
Typical Device |
| HA |
Hearing Aid |
Acceptor |
Any of the 4 HA types above |
| HAUC |
Hearing Aid Unicast Client |
Initiator |
Phone / tablet streaming audio to HA |
| HABS |
Hearing Aid Broadcast Sender |
Initiator (broadcast) |
TV streamer, loop transmitter |
| HARC |
Hearing Aid Remote Controller |
Controller |
Remote control app, stand-alone remote |
The HARC role is important because it breaks vendor lock-in for accessories. Previously, a hearing aid remote from brand X only worked with brand X hearing aids. With HAP, any HARC-compliant app or device can send volume control and preset commands to any HA-compliant hearing aid.
Hearing Aid Presets (HAS Feature)
Presets are proprietary audio processing configurations stored in the hearing aid — tuned for environments like restaurants, offices, or quiet rooms. HAS defines a numbering scheme and a Friendly Name mechanism so any app can list and switch presets without knowing the internal DSP details. Presets can also be dynamic: a telecoil preset disappears from the list when no telecoil loop is detected.
Preset Selection Flow — HARC → HA
📱
HARC App
Lists presets
by number + name
Write Preset Control Point
→
🎧
HA (Left)
Applies preset,
relays to Right via
non-BT radio
🎧
HA (Right)
Receives relayed
preset command
The “Synchronized Locally” flag tells the phone it does not need to send the command twice.
BlueZ — Discovering a HAP Device
In BlueZ, HAP support landed in 5.65+. The HAS (Hearing Access Service) is exposed as a GATT service. Here is how you scan for a hearing aid that advertises the HA role and read its Active Preset index using the D-Bus GATT API:
BlueZ — Read Active Preset Index from HAS (D-Bus / Python)
/* BlueZ profiles/audio/has.c — simplified concept */ /* The Hearing Access Service UUID: 0x854B */ #include <stdio.h> #include “lib/bluetooth.h” #include “lib/hci.h” #include “gdbus/gdbus.h” /* HAS Characteristic UUIDs */ #define HAS_UUID “0000854b-0000-1000-8000-00805f9b34fb” #define ACTIVE_PRESET_INDEX_UUID “00002bdc-0000-1000-8000-00805f9b34fb” #define PRESET_CONTROL_POINT_UUID “00002bde-0000-1000-8000-00805f9b34fb” /* Opcode for “Set Active Preset” from HAS spec */ #define HA_OPCODE_SET_ACTIVE_PRESET 0x04 #define HA_OPCODE_NEXT_PRESET 0x01 #define HA_OPCODE_PREV_PRESET 0x02 struct has_client { struct bt_gatt_client *gatt; uint16_t preset_cp_handle; uint16_t active_idx_handle; uint8_t active_preset_index; }; /* * Called when Active Preset Index characteristic * notification arrives from the hearing aid. */ static void active_preset_notify(uint16_t value_handle, const uint8_t *value, uint16_t length, void *user_data) { struct has_client *client = user_data; if (length < 1) return; client->active_preset_index = value[0]; printf(“HA active preset changed to: %u\n”, client->active_preset_index); } /* * Write to Preset Control Point to switch to preset #3. * Format: [Opcode(1 byte)] [Index(1 byte)] */ static void set_active_preset(struct has_client *client, uint8_t preset_index) { uint8_t cmd[2] = { HA_OPCODE_SET_ACTIVE_PRESET, preset_index }; bt_gatt_client_write_value(/* … */ client->gatt, client->preset_cp_handle, cmd, sizeof(cmd), NULL, NULL, NULL); }
bluetoothctl — Scan and Connect to a Hearing Aid
# Start bluetoothctl and enable LE scanning $ bluetoothctl [bluetooth]# power on [bluetooth]# scan on # You should see HAP devices advertise with the HAS UUID (0x854B) # Once device address appears (e.g. AA:BB:CC:DD:EE:FF): [bluetooth]# connect AA:BB:CC:DD:EE:FF [AA:BB:CC:DD:EE:FF]# menu gatt [AA:BB:CC:DD:EE:FF]# list-attributes # Look for the Hearing Access Service (UUID 854b) # and the Active Preset Index characteristic (UUID 2bdc) [AA:BB:CC:DD:EE:FF]# select-attribute /org/bluez/hci0/…/char0xyz [AA:BB:CC:DD:EE:FF]# read # Returns current active preset number (e.g. 0x03 = Preset 3) [AA:BB:CC:DD:EE:FF]# notify on # Now you receive notifications when the HA changes preset internally
Part 2 — TMAP: Telephony and Media Audio Profile
TMAP bundles telephony and media streaming into a single profile document. Think of it as the successor to HFP + A2DP for LE Audio. Unlike HAP, it does not introduce any new GATT characteristics — all the heavy lifting is done by BAP and CAP. The only new GATT element is the TMAP Role characteristic (inside the minimal TMAS service), which a device reads to find out which of the six roles the remote device supports before starting any audio procedures.
TMAP is intentionally flexible — a soundbar can implement only the Broadcast Media Receiver role, a phone might implement Call Gateway + Unicast Media Sender, and headphones can implement all three Acceptor roles.
TMAP — Six Roles Across Three Use Cases
📞 Telephony
CG — Call Gateway
Initiator · Phone / PC · Connects to telephone network · Must run CCP Server
CT — Call Terminal
Acceptor · Headset / mic · Bidirectional 32kHz audio (32_1, 32_2 Low Latency)
🎵 Unicast Media
UMS — Unicast Media Sender
Initiator · Phone / laptop · Must support 48_2 codec + one of 48_4 / 48_6
UMR — Unicast Media Receiver
Acceptor · Headphones · Must support all 48_1 to 48_6 codec configs
📡 Broadcast
BMS — Broadcast Media Sender
Initiator · Sends 48_1 / 48_2 BIG · Also needs one 7.5ms + one 10ms 48kHz config
BMR — Broadcast Media Receiver
Acceptor · Headphones / speaker · Must accept all 48kHz LL + HR configs. 20ms Presentation Delay
Codec Quality Levels at a Glance
TMAP Mandatory Codec Configurations
| Role Pair |
Sampling Rate |
Frame Duration |
BAP Config Name |
Notes |
| CG / CT |
32 kHz |
7.5ms + 10ms |
32_1, 32_2 |
Superwideband. Matches 5G EVS voice quality. |
| UMS (source) |
48 kHz |
10ms + one of {20ms,15ms} |
48_2 mandatory + 48_4 or 48_6 |
Application picks actual config per ASE. |
| UMR (sink) |
48 kHz |
7.5ms and 10ms all variants |
48_1 through 48_6 — all six |
Sink must accept whatever UMS sends. |
| BMS |
48 kHz |
48_1+48_2 mandatory; needs one 7.5ms + one 10ms |
48_1/2 + one of {48_3,48_5} + one of {48_4,48_6} |
Cinema, personal broadcast. |
| BMR |
48 kHz |
All Low Latency + High Reliability |
48_1_1 to 48_6_1 and 48_1_2 to 48_6_2 |
20ms Presentation Delay in both LL and HR modes. |
One subtlety worth noting: all TMAP roles must support the 2M PHY. At 48kHz with stereo, the LC3 bitrate can hit 160 kbps. The 1M PHY gives you roughly 1 Mbps raw, but with Bluetooth overhead you run out of airtime fast when multiple CISes share the same interval. 2M PHY roughly halves transmission time, giving the scheduler room to fit everything in.
BlueZ — Reading the TMAP Role Characteristic
BlueZ — TMAS Service and TMAP Role Characteristic (C)
/* * BlueZ profiles/audio/tmap.c — simplified excerpt * TMAP Role characteristic (UUID 0x2B51) carries a 16-bit bitmask * telling us which roles the remote device supports. */ #define TMAS_UUID “00001855-0000-1000-8000-00805f9b34fb” #define TMAP_ROLE_UUID “00002b51-0000-1000-8000-00805f9b34fb” /* TMAP Role bitmask values (TMAP spec Table 4.1) */ #define TMAP_ROLE_CG (1 << 0) /* Call Gateway */ #define TMAP_ROLE_CT (1 << 1) /* Call Terminal */ #define TMAP_ROLE_UMS (1 << 2) /* Unicast Media Sender */ #define TMAP_ROLE_UMR (1 << 3) /* Unicast Media Receiver */ #define TMAP_ROLE_BMS (1 << 4) /* Broadcast Media Sender */ #define TMAP_ROLE_BMR (1 << 5) /* Broadcast Media Receiver */ static void tmap_role_read_cb(bool success, uint8_t att_ecode, const uint8_t *value, uint16_t length, void *user_data) { uint16_t role_mask; if (!success || length < 2) { fprintf(stderr, “TMAP Role read failed: 0x%02x\n”, att_ecode); return; } /* Little-endian 16-bit read */ role_mask = value[0] | (value[1] << 8); printf(“Remote TMAP roles:\n”); if (role_mask & TMAP_ROLE_CG) puts(” Call Gateway (CG)”); if (role_mask & TMAP_ROLE_CT) puts(” Call Terminal (CT)”); if (role_mask & TMAP_ROLE_UMS) puts(” Unicast Media Sender (UMS)”); if (role_mask & TMAP_ROLE_UMR) puts(” Unicast Media Receiver (UMR)”); if (role_mask & TMAP_ROLE_BMS) puts(” Broadcast Media Sender (BMS)”); if (role_mask & TMAP_ROLE_BMR) puts(” Broadcast Media Receiver (BMR)”); } /* Trigger the read after discovering TMAS service */ static void tmap_read_role(struct bt_gatt_client *gatt, uint16_t role_handle) { bt_gatt_client_read_value(gatt, role_handle, tmap_role_read_cb, NULL, NULL); }
Part 3 — PBP: Public Broadcast Profile
PBP is the simplest of the three profiles, but it solves a specific interoperability problem in the broadcast world. When a BLE Audio Broadcast Source sends a BIG, there is no pairing, no connection, no negotiation — the receiver just scans and synchronises. Without PBP, a scanner that wants to know “can this source send audio I am able to decode?” must synchronise to the Periodic Advertising train, wait for the BASE to arrive, parse it, and only then decide whether to set up an audio path. That is power-hungry and slow.
PBP’s answer is simple: add a Public Broadcast Service UUID right inside the Extended Advertisement. A scanner reads that UUID in a single scan event — no PA sync needed — and immediately knows the source is using a BAP-mandated codec config that any compliant Broadcast Sink can handle. Only if the scanner wants to tune in does it then go through PA synchronisation.
PBP — How the Extended Advertisement Filter Works
📡
PBS
Public Broadcast Source
Extended Advertisement contains:
Basic Audio Announcement UUID
+ PBP Service UUID (new)
PBP UUID is only present if at least one BIS uses a BAP-mandatory codec config (e.g. 16_2_1, 24_2_1, 48_1_1, 48_2_1, 48_3_1, 48_4_1, 48_5_1, 48_6_1).
↓ Scanned by
✅ PBK / PBA sees PBP UUID
Knows source is compatible. Decides to synchronise to Periodic Advertising. No wasted scan time.
⏭ No PBP UUID in ext adv
Source may use proprietary or non-mandatory codec configs. Sink can skip it — no PA sync, no BASE parsing required.
PBP Roles (There Are Only Three)
PBP Role Summary
| Role |
Abbreviation |
Requirement |
| Public Broadcast Source |
PBS |
Must include PBP UUID in ext adv only when a BAP-mandatory codec config is present in the BIG. |
| Public Broadcast Sink |
PBK |
Must be able to recognise and interpret the PBP UUID in ext adv to filter sources. |
| Public Broadcast Assistant |
PBA |
Same as PBK — reads the UUID, helps a Broadcast Sink locate compatible sources. |
BlueZ — Scanning for PBP Sources in an Extended Advertisement
BlueZ — Filter Ext Adv for PBP UUID During LE Scan (C via HCI)
/* * Conceptual: how BlueZ tools/bap-test.c style scanning * checks for the PBP Service UUID in Extended Advertising Reports. * * PBP Service UUID: 0x1856 (16-bit assigned number, pending final assignment) * In Extended Adv it appears as a Complete List of 16-bit UUIDs (AD type 0x03) * or as a Service Data AD structure. */ #define PBP_SERVICE_UUID 0x1856 static bool adv_has_pbp_uuid(const uint8_t *adv_data, uint8_t adv_len) { uint8_t i = 0; while (i < adv_len) { uint8_t len = adv_data[i]; uint8_t type = adv_data[i + 1]; /* AD type 0x03 = Complete List of 16-bit UUIDs */ if (type == 0x03) { uint8_t j; for (j = 2; j < len; j += 2) { uint16_t uuid = adv_data[i + j] | (adv_data[i + j + 1] << 8); if (uuid == PBP_SERVICE_UUID) return true; } } i += len + 1; } return false; } /* Called inside the LE Meta Extended Advertising Report handler */ static void handle_ext_adv_report(const uint8_t *adv, uint8_t len, const bdaddr_t *addr) { if (!adv_has_pbp_uuid(adv, len)) { /* Not a PBP source — skip, save power */ return; } printf(“Found PBP-compliant Broadcast Source: %s\n”, bt_bdaddr_to_str(addr)); /* * Now safe to sync to Periodic Advertising and parse * the BASE to find the BIS audio streams. */ start_pa_sync(addr); }
bluetoothctl — Scan for Broadcast Sources (PBP Filter)
# bluetoothctl now has BAP broadcast source/sink support # Use the bap-test tool from BlueZ tools/ for full broadcast flows $ bluetoothctl [bluetooth]# power on [bluetooth]# scan on # Any device advertising the PBP UUID (0x1856) will show up. # To inspect the raw Extended Advertising data: $ sudo btmon & $ sudo hcitool lescan –extended # In btmon output, look for: # > HCI Event: LE Meta Event (0x3e) # LE Extended Advertising Report (0x0d) # … # Data length: XX # Flags: … # Complete list of 16-bit UUIDs: 0x1856 <– this is PBP! # Service Data (UUID 0x1856): … # Once source identified, sync to Periodic Advertising: $ sudo btmgmt –index 0 add-ext-adv-params … # Or use the BlueZ bap-test tool directly: $ sudo tools/bap-test -d AA:BB:CC:DD:EE:FF -S # -S = operate as Broadcast Sink, auto-sync to first PBP source found
Quick Comparison — HAP vs TMAP vs PBP
Profile Comparison at a Glance
| Property |
HAP |
TMAP |
PBP |
| Has a companion GATT Service? |
✅ HAS (new Presets char) |
✅ TMAS (Role char only) |
❌ No — broadcast only |
| Connection required? |
✅ Yes (unicast) |
✅ Yes (unicast + optional bcast) |
❌ No connection ever |
| Peak codec QoS |
24_2 (music) / 16_2 (voice) |
48_6 (unicast) / 48kHz all (bcast) |
BAP-mandatory only |
| Key new concept |
Presets + Synchronized Locally |
Role bitmask char, portmanteau roles |
PBP UUID in Extended Adv as filter |
| Primary use case |
Hearing aids + accessories |
Headsets, headphones, speakers |
Public venues, audio sharing |
Continue the BLE Audio Series
This post covered the top-level profiles. Earlier posts in the series cover BAP, PACS, ASCS, CCP, MCP, and the full Generic Audio Framework that these profiles build on.
← BLE Audio Series Index EmbeddedPathashala Home