Bluetooth GAVDP & A2DP bluez tutorial

Bluetooth GAVDP & A2DP — Audio Streaming Guide
GAVDP Streaming Channels · A2DP High Quality Audio · AVDTP Signalling · SBC Codec · Full BlueZ C Code on Linux
🎵
A2DP
📡
GAVDP
🔊
AVDTP
🎛️
SBC Codec

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.

💡 Series note: This post is part of the Bluetooth Classic protocol series. Familiarity with L2CAP channels is recommended. HFP and OPP were covered in the previous posts.

Key Terms in This Post

GAVDP A2DP AVDTP Initiator (INT) Acceptor (ACP) Stream End Point (SEP) SBC Codec AAC aptX ACL Link Streaming Channel Codec Negotiation BlueZ PipeWire PulseAudio bluetoothctl

1. Why A2DP Exists — ACL vs SCO for Audio
SCO Limitations ACL for Audio Codec Compression High Quality Audio

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
⚠️ Important: When you connect Bluetooth headphones and switch from music (A2DP) to a call (HFP), the audio path changes completely — the device drops the ACL audio stream and opens an SCO/eSCO voice channel. This is why call audio quality drops when using Bluetooth headsets.

2. GAVDP — Generic Audio/Video Distribution Profile
Initiator (INT) Acceptor (ACP) AVDTP Stream Setup

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:

  • Starts the GAVDP signalling procedure
  • Discovers stream end points (SEPs) on acceptor
  • Proposes codec and stream parameters
  • Sends Start Streaming command
  • Controls the stream (suspend, abort, change params)

Typical example: Laptop playing music → sends to headphones

What it does:

  • Responds to all requests from the INT
  • Advertises its stream end points and capabilities
  • Accepts or rejects codec/parameter proposals
  • Receives the audio/video stream
  • Decodes and outputs the audio

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

AVDTP Signalling Channel

AVDTP Media Transport Channel

📌 Two L2CAP channels: AVDTP uses two separate L2CAP channels — one for signalling (negotiation, start/stop commands) and one for actual media transport (the compressed audio data). Both are opened over the same ACL link.

3. GAVDP Features and Procedures
Stream Setup Start Streaming Suspend Abort Security Control

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
AVDTP_SUSPEND
SUSPENDED
AVDTP_START ↑ to resume
AVDTP_CLOSE
CLOSED
ABORT can be sent from any state to return to IDLE immediately

4. A2DP — Advanced Audio Distribution Profile
Source (SRC) Sink (SNK) SBC Codec Codec Negotiation 44.1 kHz Stereo

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.

  • Encodes audio using the negotiated codec
  • Sends encoded audio packets over AVDTP media channel
  • Controls playback (start, pause, stop)

Examples: Phone, laptop, tablet, MP3 player

The device that receives the audio and plays it.

  • Decodes the incoming encoded audio
  • Outputs audio through speaker or DAC
  • Advertises its supported codecs via SDP

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+.

5. AVDTP Signalling — How a Stream is Set Up
AVDTP_DISCOVER GET_CAPABILITIES SET_CONFIGURATION AVDTP_OPEN AVDTP_START

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 🎵

6. A2DP on Linux — BlueZ + PipeWire / PulseAudio
bluetoothctl PipeWire PulseAudio pactl BlueZ D-Bus

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;
}
  1. Install: sudo apt install libglib2.0-dev
  2. Compile: gcc avdtp_info.c -o avdtp_info `pkg-config --cflags --libs gio-2.0`
  3. Connect your BT speaker first via bluetoothctl
  4. 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"

7. SBC Codec — How Bluetooth Audio Compression Works
Sub-Band Coding Bitpool Sample Rate Block Length

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;
}
  1. Install: sudo apt install libsbc-dev
  2. Compile: gcc sbc_encode_demo.c -o sbc_encode_demo -lsbc
  3. Generate test PCM: ffmpeg -i music.mp3 -f s16le -ar 44100 -ac 2 input.raw
  4. Encode: ./sbc_encode_demo input.raw output.sbc

8. Quick Reference — GAVDP & A2DP
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.

Go to Courses BLE Deep Dive →

Leave a Reply

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