What This Post Covers
Hello welcome to Bluetooth GAVDP & A2DP bluez tutorial in This post covers two Bluetooth Classic profiles that power wireless audio streaming: GAVDP (Generic Audio/Video Distribution Profile) and A2DP (Advanced Audio Distribution Profile). GAVDP is the foundation — it defines the streaming channel setup and control procedures using AVDTP. A2DP builds on top of GAVDP and adds high-quality stereo audio streaming with codec negotiation.
Every developer who has ever used Bluetooth headphones, wireless speakers, or a car audio system has used A2DP without knowing it. This post explains exactly how it works under the hood, and how to work with it on Linux using BlueZ.
Key Terms in This Post
HFP (Hands-Free Profile) uses SCO/eSCO links for voice calls. SCO is designed for voice — narrow bandwidth, low latency, fixed bitrate. It works fine for phone calls but cannot carry high quality stereo music. The bandwidth is simply too low.
A2DP solves this by using ACL links instead of SCO. ACL links have much higher bandwidth — but there is a catch: even ACL links cannot carry raw uncompressed CD-quality audio in real time. A stereo 44.1 kHz 16-bit audio stream needs about 1.4 Mbps raw. Classic Bluetooth ACL can typically sustain around 700 kbps in practice. So A2DP uses a codec to compress the audio before sending and decompress it at the receiver.
SCO (HFP Voice) vs ACL (A2DP Music) — Key Differences
| Property | SCO / eSCO (HFP) | ACL (A2DP) |
|---|---|---|
| Link type | Synchronous | Asynchronous |
| Max bandwidth | 64 kbps (SCO) ~300 kbps (eSCO) |
Up to ~700 kbps practical |
| Audio quality | Narrowband voice (8 kHz sample rate) |
Stereo music (44.1 / 48 kHz) |
| Retransmission | No (real-time priority) | Yes (reliability) |
| Codec used | CVSD or mSBC | SBC, AAC, aptX, LDAC |
| Profile using it | HFP, HSP | A2DP |
GAVDP is not an end-user profile. It is a base profile that defines the infrastructure needed for any Bluetooth audio/video streaming. A2DP (audio) and VDP (video) are both built on top of GAVDP. GAVDP uses AVDTP (Audio/Video Distribution Transport Protocol) over L2CAP for all its signalling and data transport.
GAVDP Roles — Initiator and Acceptor
| 💻 Initiator (INT) | 🎧 Acceptor (ACP) |
|
What it does:
Typical example: Laptop playing music → sends to headphones |
What it does:
Typical example: Stereo headphones receiving from laptop |
Figure 1 — GAVDP Protocol Stack (Laptop → Headphones)
| 💻 Laptop (Initiator) | 🎧 Stereo Headphones (Acceptor) | ||
|
Application
(Initiator Role — sends Request) |
Application
(Acceptor Role — sends Response) |
||
|
SDP
|
SDP
|
||
|
AVDTP (Signalling + Media transport)
|
AVDTP (Signalling + Media transport)
|
||
|
L2CAP
|
L2CAP
|
||
|
Lower Layers — LMP · Baseband · Radio
|
|||
|
|||
GAVDP groups its functionality into three categories: Connection (setting up the stream), Transfer Control (managing the stream while it runs), and Signalling Control (handling error recovery). Each category contains specific procedures.
Table 1 — GAVDP Features and Procedures
| Category | Procedure | Purpose |
|---|---|---|
| 🔗 Connection | Connection Establishment | Discover stream end points (SEPs) on the acceptor, get capabilities, configure codec and stream parameters |
| Start Streaming | Both devices are configured and ready — this command starts or resumes the actual media data flow | |
| Connection Release | Cleanly tears down the streaming connection — closes media transport channel and releases resources | |
| 🎛️ Transfer Control | Suspend | Pauses the A/V stream without releasing the connection — stream can be resumed quickly without re-negotiating |
| Change Parameters | Modifies service parameters of an existing stream (e.g. change bitrate or sample rate) without full reconnection | |
| ⚠️ Signalling Control | Abort | Emergency recovery — used when a signalling message is lost or an unrecoverable error occurs on either side |
| 🔒 Security | Security Control | Exchanges content protection messages between devices (e.g. SCMS-T copy protection for audio content) |
Figure 2 — AVDTP Stream State Machine
|
IDLE
|
||
|
AVDTP_DISCOVER + GET_CAPABILITIES + SET_CONFIGURATION
|
||
|
CONFIGURED
|
||
|
AVDTP_OPEN — opens media transport L2CAP channel
|
||
|
OPEN
|
||
|
AVDTP_START — audio data begins flowing
|
||
|
STREAMING
|
||
|
||
| ABORT can be sent from any state to return to IDLE immediately |
A2DP is the profile most people know as “Bluetooth music”. It is built directly on top of GAVDP and adds one important specialisation: it defines the codec negotiation and audio format requirements for distributing high quality stereo audio. It defines its own two roles — Source and Sink — which map to the GAVDP Initiator and Acceptor roles.
A2DP Roles — Source and Sink
| 🎵 Source (SRC) | 🔊 Sink (SNK) |
|
The device that produces the audio and sends it.
Examples: Phone, laptop, tablet, MP3 player |
The device that receives the audio and plays it.
Examples: Wireless headphones, BT speakers, car audio |
A2DP Supported Codecs
A2DP mandates that every device must support SBC. All other codecs are optional. During connection setup, the source and sink negotiate which codec to use based on what both support.
A2DP Codec Comparison
| Codec | Mandatory | Typical Bitrate | Notes |
|---|---|---|---|
| SBC | ✅ Yes | ~328 kbps max | Sub-Band Codec — mandatory baseline. Good quality, moderate latency. Supported by every A2DP device. |
| AAC | Optional | ~250 kbps | Better quality than SBC at same bitrate. Default on Apple devices. Common on Android too. |
| aptX | Optional | ~352 kbps | Qualcomm codec. Lower latency than SBC. CD-like quality. Requires license. |
| aptX HD | Optional | ~576 kbps | 24-bit audio over Bluetooth. Higher quality than standard aptX. |
| LDAC | Optional | Up to 990 kbps | Sony’s Hi-Res codec. Highest quality Bluetooth audio available. Supported by Android 8.0+. |
Before a single byte of audio data flows, AVDTP goes through a signalling sequence to discover what the remote device supports, agree on a codec and parameters, and open the media transport channel. This all happens on the AVDTP signalling L2CAP channel (PSM 0x0019).
Figure 3 — AVDTP Stream Setup Signalling Flow
| 🎵 Source — Phone | 🔊 Sink — Speaker |
| ACL connection already exists. Source opens signalling L2CAP channel (PSM 0x0019) | |
|
AVDTP_DISCOVER |
|
|
AVDTP_DISCOVER_RSP (SEP list: SEID=1, Audio Sink) |
|
|
AVDTP_GET_CAPABILITIES (SEID=1) |
|
|
AVDTP_GET_CAP_RSP (SBC: 44.1kHz, stereo, bitpool 53) |
|
|
AVDTP_SET_CONFIGURATION (SBC params chosen) |
|
|
AVDTP_SET_CONFIGURATION_RSP (Accept) |
|
|
AVDTP_OPEN (SEID=1) |
|
| New L2CAP channel opened for media transport (second channel) | |
|
AVDTP_START |
|
| ✅ SBC-encoded audio packets flow on media transport channel 🎵 | |
On Linux, A2DP is handled by BlueZ for the Bluetooth layer and PipeWire (or PulseAudio on older systems) for the audio layer. BlueZ manages the AVDTP signalling and exposes the audio device over D-Bus. PipeWire then registers as the audio sink/source and handles codec encoding/decoding via its Bluetooth plugin.
Connect A2DP Speaker and Play Audio
# Step 1: Pair and connect the speaker bluetoothctl [bluetooth]# scan on [bluetooth]# pair 00:1A:7D:DA:71:11 [bluetooth]# connect 00:1A:7D:DA:71:11 [bluetooth]# trust 00:1A:7D:DA:71:11 [bluetooth]# quit # Step 2: Check that BlueZ registered the A2DP profile bluetoothctl info 00:1A:7D:DA:71:11 # Look for: # UUID: Advanced Audio (0000110d-...) <-- A2DP # UUID: A/V Remote Control (0000110e-...) <-- AVRCP # Step 3: Set the BlueZ audio profile explicitly to A2DP (not HFP) bluetoothctl [bluetooth]# select-transport 00:1A:7D:DA:71:11 # If it defaults to HFP, force A2DP: bluetoothctl [bluetooth]# connect 00:1A:7D:DA:71:11 a2dp_sink
Route Audio to Bluetooth Speaker — PulseAudio
# List all audio sinks — find your BT speaker pactl list sinks short # Output example: # 0 alsa_output.pci-0000_00_1f.3.analog-stereo ... # 1 bluez_sink.00_1A_7D_DA_71_11.a2dp_sink ... <-- this one # Move currently playing audio to BT speaker pactl move-sink-input 0 bluez_sink.00_1A_7D_DA_71_11.a2dp_sink # Set BT speaker as the default sink pactl set-default-sink bluez_sink.00_1A_7D_DA_71_11.a2dp_sink # Play a test sound to verify paplay /usr/share/sounds/freedesktop/stereo/audio-test-signal.oga
Route Audio — PipeWire (Modern Ubuntu / Fedora)
# PipeWire replaces PulseAudio on modern distros # List available Bluetooth audio sinks wpctl status | grep -A5 "Bluetooth" # Set BT speaker as default output wpctl set-default <sink-id-from-above> # Check current A2DP codec in use pactl list cards | grep -A 20 "bluez" # Look for: # Active Profile: a2dp-sink-sbc (SBC codec active) # Available profiles: # a2dp-sink-sbc: High Fidelity Playback (SBC) # a2dp-sink-aac: High Fidelity Playback (AAC) # Force AAC codec (if both devices support it) pactl set-card-profile bluez_card.00_1A_7D_DA_71_11 a2dp-sink-aac
Read AVDTP Transport Info via BlueZ D-Bus
/*
* avdtp_info.c — Read A2DP transport properties via BlueZ D-Bus
* Shows codec, state, and configuration of the active A2DP transport
* Compile: gcc avdtp_info.c -o avdtp_info `pkg-config --cflags --libs gio-2.0`
* Run: ./avdtp_info
*/
#include <gio/gio.h>
#include <stdio.h>
int main() {
GError *error = NULL;
/* Connect to the system D-Bus where BlueZ lives */
GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
if (!conn) {
fprintf(stderr, "System bus error: %s\n", error->message);
return 1;
}
/*
* Get all managed BlueZ objects — this returns every BT object
* including MediaTransport1 objects for active A2DP streams.
* MediaTransport1 is the D-Bus object for an AVDTP media transport.
*/
GVariant *objects = g_dbus_connection_call_sync(
conn,
"org.bluez",
"/",
"org.freedesktop.DBus.ObjectManager",
"GetManagedObjects",
NULL,
G_VARIANT_TYPE("(a{oa{sa{sv}}})"),
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error
);
if (!objects) {
fprintf(stderr, "GetManagedObjects failed: %s\n", error->message);
return 1;
}
GVariantIter *obj_iter;
const char *obj_path;
GVariant *ifaces;
g_variant_get(objects, "(a{oa{sa{sv}}})", &obj_iter);
while (g_variant_iter_next(obj_iter, "{oa{sa{sv}}}", &obj_path, &ifaces)) {
GVariantIter *iface_iter;
const char *iface_name;
GVariant *props;
g_variant_get(ifaces, "a{sa{sv}}", &iface_iter);
while (g_variant_iter_next(iface_iter, "{sa{sv}}", &iface_name, &props)) {
/* Look for MediaTransport1 — this is an active A2DP stream */
if (g_strcmp0(iface_name, "org.bluez.MediaTransport1") == 0) {
printf("\n=== A2DP Transport Found: %s ===\n", obj_path);
/* Read State property: idle / pending / active */
GVariant *state = g_variant_lookup_value(props,
"State", G_VARIANT_TYPE_STRING);
if (state) {
printf(" State: %s\n",
g_variant_get_string(state, NULL));
g_variant_unref(state);
}
/* Read Codec property: 0x00=SBC, 0x02=AAC, etc. */
GVariant *codec = g_variant_lookup_value(props,
"Codec", G_VARIANT_TYPE_BYTE);
if (codec) {
uint8_t c = g_variant_get_byte(codec);
printf(" Codec: 0x%02x (%s)\n", c,
c == 0x00 ? "SBC" :
c == 0x02 ? "AAC" :
c == 0xff ? "Vendor (aptX/LDAC)" : "Unknown");
g_variant_unref(codec);
}
/* Read Volume if available */
GVariant *vol = g_variant_lookup_value(props,
"Volume", G_VARIANT_TYPE_UINT16);
if (vol) {
printf(" Volume: %u\n", g_variant_get_uint16(vol));
g_variant_unref(vol);
}
}
g_variant_unref(props);
}
g_variant_iter_free(iface_iter);
g_variant_unref(ifaces);
}
g_variant_iter_free(obj_iter);
g_variant_unref(objects);
g_object_unref(conn);
return 0;
}
- Install:
sudo apt install libglib2.0-dev - Compile:
gcc avdtp_info.c -o avdtp_info `pkg-config --cflags --libs gio-2.0` - Connect your BT speaker first via bluetoothctl
- Run:
./avdtp_info
Monitor AVDTP Signalling with btmon
# btmon captures all HCI traffic including AVDTP signalling and media data sudo btmon # Then in another terminal connect your BT speaker: bluetoothctl connect 00:1A:7D:DA:71:11 # btmon output will show: # > ACL Data RX: Handle 11 flags 0x02 dlen 26 # Channel: 64 len 22 [PSM 25 mode 0] <-- AVDTP signalling (PSM 0x0019=25) # AVDTP: Discover (Signal id 0x01) # # > ACL Data TX: Handle 11 flags 0x00 dlen 24 # Channel: 64 len 20 [PSM 25 mode 0] # AVDTP: Discover Response # Stream End Point: SEID 1 (Audio Sink) # # > ACL Data TX: Handle 11 flags 0x00 dlen 1021 # Channel: 65 len 1017 [PSM 25 mode 0] <-- AVDTP media channel # (SBC encoded audio packet) # To filter only AVDTP traffic: sudo btmon | grep -A3 "AVDTP"
SBC (Sub-Band Codec) is the only mandatory codec in A2DP. It splits audio into frequency sub-bands, quantises each band independently based on how perceptually important it is, and packs the result into compact frames. The key configurable parameter is the bitpool value — higher bitpool = better quality but more bandwidth used.
SBC Configuration Parameters
| Parameter | Common Values | Effect |
|---|---|---|
| Sample Rate | 44100 Hz / 48000 Hz | 44.1 kHz = CD quality. 48 kHz = studio / video quality. |
| Channel Mode | Joint Stereo / Stereo | Joint Stereo gives better quality at same bitrate by encoding L+R sum and difference. |
| Subbands | 4 or 8 | 8 subbands = better quality, more CPU. Most devices use 8. |
| Block Length | 4 / 8 / 12 / 16 | Number of audio blocks per SBC frame. Larger = more efficient but more latency. |
| Bitpool | 2–53 (standard range) | The most impactful parameter. Higher = more bits per frame = better quality, more bandwidth. 53 is the recommended max for high quality stereo. |
SBC Bitpool Value Guide (Joint Stereo, 44.1 kHz, 8 subbands)
|
2–17
Poor
|
18–35
Acceptable
|
36–53
High Quality
|
53 (max)
~328 kbps — best
|
BlueZ default bitpool = 53 |
Check and Set SBC Bitpool in BlueZ
# Check current SBC configuration (PipeWire / PulseAudio) pactl list cards | grep -A 30 "bluez" | grep -i "sbc\|codec\|bitpool" # Set higher SBC quality in /etc/bluetooth/audio.conf (PulseAudio module) # sudo nano /etc/bluetooth/audio.conf # Add under [A2DP]: # SBCMinBitpool=53 # SBCMaxBitpool=53 # For PipeWire — edit: /usr/share/pipewire/media-session.d/bluez-monitor.conf # Under [bluez5]: # sbc_min_bitpool = 53 # sbc_max_bitpool = 53 # Restart audio daemon after changes systemctl --user restart pipewire pipewire-pulse
Encode Audio to SBC in C
/*
* sbc_encode_demo.c — Encode raw PCM audio to SBC frames
* Uses the sbc library from BlueZ (libbluetooth-dev or libsbc-dev)
* Compile: gcc sbc_encode_demo.c -o sbc_encode_demo -lsbc
* Run: ./sbc_encode_demo input.raw output.sbc
*
* input.raw = raw signed 16-bit little-endian stereo PCM at 44100 Hz
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sbc/sbc.h> /* BlueZ SBC library */
#define FRAME_LEN 512 /* PCM input bytes per encode call */
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Usage: %s input.raw output.sbc\n", argv[0]);
return 1;
}
FILE *fin = fopen(argv[1], "rb");
FILE *fout = fopen(argv[2], "wb");
if (!fin || !fout) { perror("fopen"); return 1; }
sbc_t sbc;
sbc_init(&sbc, 0L);
/* Configure SBC for high quality A2DP:
* 44.1 kHz, joint stereo, 8 subbands, 16 blocks, bitpool 53 */
sbc.frequency = SBC_FREQ_44100;
sbc.mode = SBC_MODE_JOINT_STEREO;
sbc.subbands = SBC_SB_8;
sbc.blocks = SBC_BLK_16;
sbc.bitpool = 53;
sbc.allocation = SBC_AM_LOUDNESS;
/* Calculate encoded frame size for buffer allocation */
size_t frame_len = sbc_get_frame_length(&sbc);
size_t codesize = sbc_get_codesize(&sbc); /* PCM bytes consumed per frame */
uint8_t *pcm_buf = malloc(codesize);
uint8_t *sbc_buf = malloc(frame_len);
printf("SBC config: %zu bytes PCM → %zu bytes SBC per frame\n",
codesize, frame_len);
size_t bytes_read;
size_t total_frames = 0;
while ((bytes_read = fread(pcm_buf, 1, codesize, fin)) == codesize) {
ssize_t encoded;
size_t written;
/* sbc_encode() compresses one frame of PCM into SBC */
encoded = sbc_encode(&sbc,
pcm_buf, codesize, /* input PCM */
sbc_buf, frame_len, /* output SBC */
(ssize_t *)&written);
if (encoded < 0) {
fprintf(stderr, "sbc_encode error: %zd\n", encoded);
break;
}
fwrite(sbc_buf, 1, written, fout);
total_frames++;
}
printf("Encoded %zu SBC frames.\n", total_frames);
sbc_finish(&sbc);
free(pcm_buf);
free(sbc_buf);
fclose(fin);
fclose(fout);
return 0;
}
- Install:
sudo apt install libsbc-dev - Compile:
gcc sbc_encode_demo.c -o sbc_encode_demo -lsbc - Generate test PCM:
ffmpeg -i music.mp3 -f s16le -ar 44100 -ac 2 input.raw - Encode:
./sbc_encode_demo input.raw output.sbc
| Item | Profile | Details |
|---|---|---|
| Transport protocol | GAVDP/A2DP | AVDTP over L2CAP over ACL — PSM 0x0019 |
| Two L2CAP channels | AVDTP | Channel 1: Signalling. Channel 2: Media transport |
| Mandatory codec | A2DP | SBC — every A2DP device must support it |
| Best quality codec | A2DP | LDAC up to 990 kbps (optional) |
| Pause stream | GAVDP | AVDTP_SUSPEND |
| Resume stream | GAVDP | AVDTP_START |
| Linux audio daemon | A2DP | PipeWire (modern) or PulseAudio with BlueZ module |
| D-Bus object | BlueZ | org.bluez.MediaTransport1 |
| SBC library | Linux | libsbc-dev — sbc_encode() / sbc_decode() |
| Monitor AVDTP | Debug | sudo btmon | grep AVDTP |
Next Up: AVRCP — Bluetooth Remote Control
The next post covers AVRCP (Audio/Video Remote Control Profile) — how your headphones skip tracks, pause music, and show metadata. Includes BlueZ AVCTP code and D-Bus examples. Part of the free Bluetooth course on EmbeddedPathashala.
