Chapter 53: Bluetooth A2DP Sink Implementation
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the Advanced Audio Distribution Profile (A2DP) and its role in Bluetooth Classic audio streaming.
- Explain the difference between A2DP Source and Sink roles.
- Describe how A2DP uses codecs like SBC and protocols like AVDTP.
- Initialize and configure the A2DP Sink module in ESP-IDF.
- Implement an A2DP Sink on an ESP32 to receive and process audio data.
- Handle A2DP connection and audio state events.
- Interface with an I2S audio output peripheral to play received audio.
- Test A2DP audio streaming using a smartphone or other A2DP source.
- Identify ESP32 variants that support A2DP Sink.
- Troubleshoot common issues in A2DP Sink implementations.
Introduction
In the previous chapters, we explored Bluetooth Classic fundamentals and the Serial Port Profile (SPP). Now, we turn our attention to wireless audio streaming with the Advanced Audio Distribution Profile (A2DP). A2DP is the cornerstone of Bluetooth audio, enabling devices to stream high-quality stereo audio wirelessly. Common examples include streaming music from a smartphone to Bluetooth speakers, headphones, or car audio systems.
For the ESP32, implementing an A2DP Sink (receiver) transforms it into a wireless audio playback device. This capability allows you to build custom Bluetooth speakers, integrate voice prompts or music playback into your IoT projects, or create unique audio-reactive applications. This chapter will delve into the theory behind A2DP and guide you through a practical example of building an A2DP Sink on the ESP32, capable of receiving audio from a source like a smartphone and playing it through an I2S audio interface.
Theory
What is the Advanced Audio Distribution Profile (A2DP)?
The Advanced Audio Distribution Profile (A2DP) defines the protocols and procedures that realize the distribution of high-quality audio content, either mono or stereo, between Bluetooth devices. It’s a unidirectional profile, meaning audio flows in one direction – from an A2DP Source to an A2DP Sink. A2DP operates over an Asynchronous Connection-Less (ACL) link, which is a fundamental Bluetooth Classic connection type.
A2DP Roles
A2DP defines two primary roles for devices involved in an audio stream:
- Source (SRC): This is the device that originates and transmits the audio stream. Examples include smartphones playing music, laptops, or dedicated Bluetooth audio transmitters.
- Sink (SNK): This is the device that receives and renders (plays back) the audio stream. Examples include Bluetooth headphones, wireless speakers, and, in the context of this chapter, an ESP32 programmed as an audio receiver.
This chapter focuses on implementing the A2DP Sink role on the ESP32.
Key Protocols and Components Used by A2DP
A2DP is built upon several other Bluetooth protocols and concepts:
- Generic Audio/Video Distribution Profile (GAVDP): This profile provides the foundational framework for A2DP and VDP (Video Distribution Profile). GAVDP defines the procedures for discovering, configuring, establishing, and terminating audio/video streams between devices.
- Audio/Video Distribution Transport Protocol (AVDTP): AVDTP is responsible for the actual transport of audio (and video) streams. It operates over L2CAP channels and handles:
- Signaling: Negotiation of stream parameters (e.g., codec type, sampling rate), establishment, and termination of streams.
- Transport: Packetization and transmission of the encoded audio data.
- Audio/Video Control Transport Protocol (AVCTP): While not directly part of A2DP’s audio data path, AVCTP is crucial for the Audio/Video Remote Control Profile (AVRCP). AVRCP often accompanies A2DP and allows the Sink device (or a separate controller) to send playback control commands (like play, pause, skip track, volume control) to the Source device. We will touch upon AVRCP as it’s highly relevant for a complete audio solution.
- Audio Codecs: To transmit audio efficiently, A2DP requires audio data to be compressed using a codec. The A2DP specification mandates support for one codec and allows for several optional ones:
- SBC (Low Complexity Subband Codec): This is the mandatory codec for all A2DP devices. SBC is designed to provide acceptable audio quality with relatively low computational overhead, making it suitable for resource-constrained devices. It is a lossy codec.
- MPEG-1,2 Audio (MP2, MP3): Optional. Widely known for music compression.
- MPEG-2,4 AAC (Advanced Audio Coding): Optional. Often provides better audio quality than SBC at similar bitrates. Commonly used by Apple devices.
- ATRAC (Adaptive Transform Acoustic Coding): Optional. A proprietary codec developed by Sony.
- Vendor-Specific Codecs: Manufacturers can also implement their own proprietary codecs (e.g., Qualcomm’s aptX family).The ESP-IDF’s A2DP Sink implementation primarily supports the mandatory SBC codec out-of-the-box. The stack handles the decoding of SBC data into raw PCM (Pulse Code Modulation) audio, which can then be sent to an audio output peripheral.
A2DP Connection Process (Simplified Sink Perspective)
The establishment of an A2DP stream from the Sink’s perspective generally follows these steps:
sequenceDiagram; %% A2DP Sink Connection Flow Diagram actor Source as "A2DP Source (e.g., Phone)"; actor Sink as "A2DP Sink (e.g., ESP32)"; %% Style definitions based on previous color scheme %% Primary/Start Nodes: fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 %% Process Nodes: fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF %% Success Nodes: fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 %% Decision Nodes: fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E participant Source; participant Sink; Source->>Sink: 1. Paging & Baseband Connection (ACL Link); activate Sink; Sink-->>Source: Connection Established; deactivate Sink; rect rgb(219, 234, 254) /* Light Blue for Process Nodes */ Source->>Sink: 2. SDP Query (for A2DP Sink Service UUID: 0x110B); activate Sink; Sink-->>Source: SDP Response (Sink capabilities, e.g., supported codecs); deactivate Sink; end rect rgb(219, 234, 254) /* Light Blue for Process Nodes */ Source->>Sink: 3. AVDTP Signaling Connection (L2CAP Channel); activate Sink; Sink-->>Source: Signaling Channel Established; deactivate Sink; end rect rgb(254, 243, 199) /* Light Amber for Decision/Negotiation */ Source->>Sink: 4. AVDTP: SET_CONFIGURATION (Propose audio stream: Codec SBC, Sample Rate, etc.); activate Sink; Sink-->>Source: AVDTP: ACCEPT_CONFIGURATION (or REJECT); deactivate Sink; Note over Source,Sink: Codec and parameters negotiated; end rect rgb(219, 234, 254) /* Light Blue for Process Nodes */ Source->>Sink: 5. AVDTP: OPEN (Request to open transport channel); activate Sink; Sink-->>Source: AVDTP: Transport Channel Opened; deactivate Sink; end rect rgb(209, 250, 229) /* Light Green for Success/Data Exchange */ Source->>Sink: 6. AVDTP: START (Request to start audio stream); activate Sink; Sink-->>Source: AVDTP: Stream Started Acknowledgment; deactivate Sink; loop Audio Data Streaming Source-->>Sink: AVDTP Audio Packets (Encoded SBC frames); Sink->>Sink: Receive, Decode SBC to PCM, Send to I2S; end end opt Stream Control (Pause/Stop/Abort) alt Source Initiates Control Source->>Sink: AVDTP: SUSPEND / CLOSE / ABORT; activate Sink; Sink-->>Source: AVDTP: Acknowledgment; deactivate Sink; else Sink Initiates Control (Less common for A2DP Sink to initiate stop) Sink->>Source: AVDTP: SUSPEND / CLOSE / ABORT; activate Source; Source-->>Sink: AVDTP: Acknowledgment; deactivate Source; end end Note over Source,Sink: Connection may also involve AVRCP for remote control (not shown in detail).
- Baseband Connection: The Source device typically initiates a Bluetooth baseband (ACL) connection to the Sink device. This involves paging and connection establishment. The Sink must be discoverable and connectable.
- SDP Query: The Source performs a Service Discovery Protocol (SDP) query on the Sink to determine if it supports the A2DP Sink service and to get information about its capabilities (e.g., supported codecs). The A2DP Sink service has a specific UUID (0x110B).
- AVDTP Signaling Connection: The Source initiates an AVDTP signaling connection (an L2CAP channel) to the Sink.
- Stream Configuration (AVDTP SET_CONFIGURATION): The Source proposes an audio stream configuration to the Sink. This includes selecting a codec (e.g., SBC) and its parameters (e.g., sampling frequency, channel mode, bitpool value for SBC). The Sink can accept or reject this configuration. Typically, devices negotiate to find a mutually supported configuration, starting with preferred options.
- Stream Establishment (AVDTP OPEN): Once a configuration is agreed upon, the Source requests to open the AVDTP transport channel. The Sink accepts. This establishes a dedicated L2CAP channel for audio data.
- Audio Streaming (AVDTP START & Data Transfer): The Source requests to start the audio stream. Upon acknowledgment from the Sink, the Source begins encoding audio, packetizing it according to AVDTP, and sending it over the transport channel.
- Audio Playback: The Sink receives the AVDTP packets, extracts the encoded audio frames (e.g., SBC), decodes them into PCM data, and sends this PCM data to an audio output peripheral (like an I2S DAC) for playback.
- Stream Control (AVDTP SUSPEND, CLOSE, ABORT): During the session, the stream can be suspended (paused), closed (normally terminated), or aborted (abnormally terminated) by either the Source or the Sink using AVDTP commands.
Audio Data Flow
The journey of audio data in an A2DP system is as follows:
- Source:
- Original audio (e.g., from a music file or microphone).
- If not already in the target codec format, it’s encoded (e.g., PCM to SBC, or MP3 to SBC if SBC is negotiated).
- Encoded audio frames are packetized by AVDTP.
- AVDTP packets are sent over an L2CAP channel to the Sink.
- Sink (e.g., ESP32):
- Receives AVDTP packets from the L2CAP channel.
- Extracts the encoded audio frames (e.g., SBC frames).
- Decodes the audio frames into raw PCM data. The ESP-IDF A2DP stack handles SBC decoding internally.
- The PCM data is then ready to be sent to an audio output hardware interface, typically I2S (Inter-IC Sound).
- The I2S interface routes the PCM data to an external DAC (Digital-to-Analog Converter) or, in some ESP32 variants, an internal DAC. The DAC converts the digital audio signal into an analog signal that can drive headphones or an amplifier and speaker.
graph TB; %% A2DP Audio Data Flow Diagram %% Styles classDef sourceDevice fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef process fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef transport fill:#E0E7FF,stroke:#3730A3,stroke-width:1px,color:#3730A3; classDef sinkDevice fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef output fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; subgraph "A2DP Source ( Smartphone)" direction LR SRC_AUDIO["Original Audio<br>(e.g., MP3, AAC, PCM)"] --> SRC_ENCODE["Audio Encoder<br>(e.g., to SBC if needed)"]; SRC_ENCODE --> SRC_AVDTP["AVDTP Packetizer"]; end subgraph Bluetooth Wireless Link direction TB BT_LINK["L2CAP Channel<br>(Bluetooth Classic ACL)"]; end subgraph "A2DP Sink (ESP32)" direction LR SNK_AVDTP["AVDTP Depacketizer"] --> SNK_DECODE["SBC Decoder<br>(ESP-IDF Stack)"]; SNK_DECODE --> SNK_PCM["Raw PCM Audio Data<br>(16-bit Stereo)"]; SNK_PCM --> SNK_I2S["I2S Interface<br>(ESP32 Peripheral)"]; end subgraph Audio Output Hardware direction LR HW_DAC["External I2S DAC<br>(Digital-to-Analog Converter)"] --> HW_AMP["Amplifier"]; HW_AMP --> HW_SPEAKER["Speaker / Headphones"]; end SRC_AVDTP --> BT_LINK; BT_LINK --> SNK_AVDTP; SNK_I2S --> HW_DAC; class SRC_AUDIO,SRC_ENCODE,SRC_AVDTP sourceDevice; class BT_LINK transport; class SNK_AVDTP,SNK_DECODE,SNK_PCM,SNK_I2S sinkDevice; class HW_DAC,HW_AMP,HW_SPEAKER output;
Audio Quality Considerations
The perceived audio quality in an A2DP stream depends on several factors:
- Source Audio Quality: The quality of the original audio material is paramount.
- Codec Used: Different codecs offer varying quality at different bitrates. AAC is generally considered superior to SBC at comparable bitrates.
- Codec Parameters: For SBC, parameters like sampling frequency (e.g., 44.1kHz, 48kHz), channel mode (mono, stereo, joint stereo), and especially the
bitpool
value significantly impact quality. A higher bitpool allows for a higher bitrate and better quality but consumes more bandwidth. The ESP-IDF A2DP stack typically negotiates these parameters. - Bluetooth Link Quality: A poor Bluetooth connection (e.g., due to distance or interference) can lead to packet loss, resulting in audio glitches, dropouts, or stuttering.
- Sink’s Audio Output Stage: The quality of the DAC, amplifier, and speakers used by the Sink device also plays a critical role in the final audio output.
Practical Examples
Example 1: ESP32 as an A2DP Sink
This example demonstrates how to configure the ESP32 to act as an A2DP Sink. It will:
- Initialize the Bluetooth stack (NVS, controller, Bluedroid).
- Initialize and configure the A2DP Sink profile.
- Register callback functions to handle A2DP connection events, audio state changes, and incoming audio data.
- Initialize an I2S interface to output the received audio to an external DAC or the ESP32’s internal DAC (if applicable and configured).
- When an A2DP Source (like a smartphone) connects and streams audio, the ESP32 will receive it, decode it (SBC to PCM is handled by the stack), and send the PCM data to the I2S peripheral for playback.
1. Project Setup (VS Code with ESP-IDF Extension)
- Create a new ESP-IDF project in VS Code (e.g.,
a2dp_sink_demo
). - Create or open the
sdkconfig.defaults
file in your project’s root directory and add the following configurations:CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BT_A2DP_ENABLE=y
CONFIG_BT_A2DP_SINK_ENABLE=y
CONFIG_BT_SSP_ENABLED=y
CONFIG_BT_CTRL_MODE_EFF=2
CONFIG_BT_BLUEDROID_NAME="ESP32_A2DP_Sink" # For I2S audio output (ensure I2S driver is available) # CONFIG_DRIVER_I2S_ENABLE=y # This is usually enabled by default if I2S APIs are used.
- Alternatively, configure these options using
idf.py menuconfig
:Component config
->Bluetooth
->[*] Bluetooth
Component config
->Bluetooth
->Bluedroid Options
->[*] Classic Bluetooth
Component config
->Bluetooth
->Bluedroid Options
->[*] A2DP
Component config
->Bluetooth
->Bluedroid Options
->[*] A2DP Sink (SNK)
Component config
->Bluetooth
->Bluedroid Options
->[*] Secure Simple Pairing
Component config
->Bluetooth
->Bluetooth controller
->Bluetooth controller mode (CLASSIC_BT)
- Set
(ESP32_A2DP_Sink)
underComponent config
->Bluetooth
->Bluedroid Options
->Default Bluedroid Host name
. - Ensure I2S is enabled under
Component config
->Driver configurations
->I2S Driver
->[*] Enable I2S driver
(if not already enabled).
2. C Code Implementation (main/a2dp_sink_main.c
)
The following code provides a comprehensive A2DP Sink implementation.
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stdio.h> // For sprintf
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_a2dp_api.h"
#include "esp_avrc_api.h" // For AVRCP Target (receiving remote control commands)
#include "driver/i2s.h"
static const char *TAG = "A2DP_SINK_DEMO";
// I2S Configuration
// IMPORTANT: Modify these pins based on your hardware setup (ESP32 board and external DAC)
#define I2S_NUM (I2S_NUM_0)
#define I2S_BCK_PIN (GPIO_NUM_26) // Bit Clock
#define I2S_WS_PIN (GPIO_NUM_25) // Word Select (Left/Right Clock)
#define I2S_DO_PIN (GPIO_NUM_22) // Data Out
// Data In pin is not used for A2DP Sink (TX mode)
#define I2S_DI_PIN (I2S_PIN_NO_CHANGE)
// A2DP application state machine
typedef enum {
APP_AV_STATE_IDLE,
APP_AV_STATE_UNCONNECTED,
APP_AV_STATE_CONNECTING,
APP_AV_STATE_CONNECTED,
APP_AV_STATE_DISCONNECTING,
} app_av_state_t;
// Forward declarations of callback functions and handlers
static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param);
static void bt_app_a2d_cb(esp_a2d_sink_cb_event_t event, esp_a2d_sink_cb_param_t *param);
static void bt_app_a2d_data_cb(const uint8_t *data, uint32_t len);
static void bt_app_rc_tg_cb(esp_avrc_tg_cb_event_t event, esp_avrc_tg_cb_param_t *param);
static void bt_app_av_sm_handler(uint16_t event, void *param);
// Global variables for A2DP Sink state
static esp_a2d_audio_state_t s_audio_state = ESP_A2D_AUDIO_STATE_STOPPED;
static app_av_state_t s_app_av_state = APP_AV_STATE_IDLE;
static esp_bd_addr_t s_peer_bda = {0}; // Stores the Bluetooth Device Address of the connected peer
static char s_local_bda_str[18]; // Stores local BDA as string
// Helper function to convert Bluetooth Device Address (BDA) to string
static char *bda_to_str(const esp_bd_addr_t bda, char *str, size_t size) {
if (bda == NULL || str == NULL || size < 18) {
return NULL;
}
sprintf(str, "%02X:%02X:%02X:%02X:%02X:%02X",
bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);
return str;
}
// I2S peripheral initialization
static void i2s_init(void) {
ESP_LOGI(TAG, "Initializing I2S interface...");
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), // Set as I2S master and transmitter
.sample_rate = 44100, // Default sample rate, will be updated by A2DP audio config
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // Default bits per sample
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // Stereo
.communication_format = I2S_COMM_FORMAT_STAND_I2S, // Standard I2S protocol
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // Interrupt priority
.dma_buf_count = 6, // Number of DMA buffers
.dma_buf_len = 1024, // Size of each DMA buffer in samples (increased for stability)
.use_apll = true, // Use APLL as clock source for better accuracy
.tx_desc_auto_clear = true, // Auto clear TX descriptors
.fixed_mclk = 0 // No MCLK output needed for most DACs
};
esp_err_t err = i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
if (err != ESP_OK) {
ESP_LOGE(TAG, "I2S driver install failed: %s", esp_err_to_name(err));
return;
}
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCK_PIN,
.ws_io_num = I2S_WS_PIN,
.data_out_num = I2S_DO_PIN,
.data_in_num = I2S_DI_PIN // Not used for TX mode
};
err = i2s_set_pin(I2S_NUM, &pin_config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "I2S set pin failed: %s", esp_err_to_name(err));
i2s_driver_uninstall(I2S_NUM); // Clean up if pin setting fails
return;
}
// Set initial clock rate (will be updated by A2DP callback)
err = i2s_set_clk(I2S_NUM, i2s_config.sample_rate, i2s_config.bits_per_sample, I2S_CHANNEL_STEREO);
if (err != ESP_OK) {
ESP_LOGE(TAG, "I2S set clock failed: %s", esp_err_to_name(err));
}
ESP_LOGI(TAG, "I2S initialized successfully (BCK:%d, WS:%d, DO:%d)", I2S_BCK_PIN, I2S_WS_PIN, I2S_DO_PIN);
i2s_stop(I2S_NUM); // Ensure I2S is stopped initially
}
// Main application entry point
void app_main(void) {
esp_err_t ret;
// Initialize NVS (Non-Volatile Storage) - required for Bluetooth
ESP_LOGI(TAG, "Initializing NVS...");
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "NVS initialized.");
// Release Bluetooth controller memory if it was allocated for BLE
ESP_LOGI(TAG, "Releasing BT controller memory for BLE mode...");
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
// Initialize Bluetooth Controller
ESP_LOGI(TAG, "Initializing Bluetooth controller...");
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Initialize controller failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "Bluetooth controller initialized.");
// Enable Bluetooth Controller in Classic Bluetooth mode
ESP_LOGI(TAG, "Enabling Bluetooth controller in Classic BT mode...");
ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Enable controller failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "Bluetooth controller enabled in Classic BT mode.");
// Initialize Bluedroid Host Stack
ESP_LOGI(TAG, "Initializing Bluedroid host stack...");
ret = esp_bluedroid_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Initialize Bluedroid failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "Bluedroid host stack initialized.");
// Enable Bluedroid Host Stack
ESP_LOGI(TAG, "Enabling Bluedroid host stack...");
ret = esp_bluedroid_enable();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "Bluedroid host stack enabled.");
// Register GAP callback function
ESP_LOGI(TAG, "Registering GAP callback...");
ret = esp_bt_gap_register_callback(bt_app_gap_cb);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "GAP callback register failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "GAP callback registered.");
// Register A2DP Sink event callback function
ESP_LOGI(TAG, "Registering A2DP Sink event callback...");
ret = esp_a2d_sink_register_callback(bt_app_a2d_cb);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "A2DP Sink event callback register failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "A2DP Sink event callback registered.");
// Register A2DP Sink data callback function (for receiving audio data)
ESP_LOGI(TAG, "Registering A2DP Sink data callback...");
ret = esp_a2d_sink_register_data_callback(bt_app_a2d_data_cb);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "A2DP Sink data callback register failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "A2DP Sink data callback registered.");
// Initialize A2DP Sink profile
ESP_LOGI(TAG, "Initializing A2DP Sink profile...");
ret = esp_a2d_sink_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "A2DP Sink init failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "A2DP Sink profile initialized.");
// Initialize AVRCP Target profile (for remote control commands)
ESP_LOGI(TAG, "Initializing AVRCP Target profile...");
ret = esp_avrc_tg_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "AVRCP Target init failed: %s", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Registering AVRCP Target callback...");
ret = esp_avrc_tg_register_callback(bt_app_rc_tg_cb); // Corrected: register AVRCP Target callback
if (ret != ESP_OK) {
ESP_LOGE(TAG, "AVRCP Target callback register failed: %s", esp_err_to_name(ret));
}
}
ESP_LOGI(TAG, "AVRCP Target profile initialized and callback registered.");
// Set device name (as configured in menuconfig or sdkconfig.defaults)
// This name will be visible to other Bluetooth devices during scanning.
const char * configured_device_name;
esp_bt_dev_get_device_name(&configured_device_name); // Get name set by menuconfig
ESP_LOGI(TAG, "Setting device name to '%s'...", configured_device_name);
// Note: esp_bt_dev_set_device_name() can override menuconfig if called after bluedroid_init.
// We rely on the menuconfig name here.
// Set discoverability and connectability mode
ESP_LOGI(TAG, "Setting device to connectable and discoverable...");
ret = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Set scan mode failed: %s", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Device set to connectable and discoverable.");
}
// Initialize I2S for audio output
i2s_init();
ESP_LOGI(TAG, "A2DP Sink Demo Initialized. Waiting for connections...");
// Get and log local Bluetooth device address
const uint8_t *bda = esp_bt_dev_get_address();
if (bda) {
bda_to_str(bda, s_local_bda_str, sizeof(s_local_bda_str));
ESP_LOGI(TAG, "Local Bluetooth Address: %s", s_local_bda_str);
} else {
ESP_LOGE(TAG, "Failed to get local Bluetooth address.");
}
}
// Bluetooth GAP (Generic Access Profile) callback function
static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) {
char peer_bda_str[18]; // Buffer for peer BDA string
switch (event) {
case ESP_BT_GAP_AUTH_CMPL_EVT: { // Authentication complete event
if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
ESP_LOGI(TAG, "GAP Authentication success. Device: %s, BDA: %s",
param->auth_cmpl.device_name,
bda_to_str(param->auth_cmpl.bda, peer_bda_str, sizeof(peer_bda_str)));
memcpy(s_peer_bda, param->auth_cmpl.bda, ESP_BD_ADDR_LEN); // Store peer BDA
} else {
ESP_LOGE(TAG, "GAP Authentication failed. Status: %d, BDA: %s", param->auth_cmpl.stat,
bda_to_str(param->auth_cmpl.bda, peer_bda_str, sizeof(peer_bda_str)));
}
break;
}
#if (CONFIG_BT_SSP_ENABLED == true)
case ESP_BT_GAP_CFM_REQ_EVT: // SSP (Secure Simple Pairing) User Confirmation Request
ESP_LOGI(TAG, "GAP SSP Confirmation request. BDA: %s. Numeric value: %d",
bda_to_str(param->cfm_req.bda, peer_bda_str, sizeof(peer_bda_str)),
param->cfm_req.num_val);
// For simplicity, auto-confirm. In a real product, display num_val to user.
esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
break;
case ESP_BT_GAP_KEY_NOTIF_EVT: // SSP Passkey Notification (display this passkey to the user)
ESP_LOGI(TAG, "GAP SSP Passkey notification. BDA: %s, Passkey: %d",
bda_to_str(param->key_notif.bda, peer_bda_str, sizeof(peer_bda_str)),
param->key_notif.passkey);
break;
case ESP_BT_GAP_KEY_REQ_EVT: // SSP Passkey Request (user enters passkey)
ESP_LOGI(TAG, "GAP SSP Passkey request. BDA: %s",
bda_to_str(param->key_req.bda, peer_bda_str, sizeof(peer_bda_str)));
// For simplicity, we don't handle passkey input.
// In a real app, prompt user: esp_bt_gap_ssp_passkey_reply(param->key_req.bda, true, 123456);
ESP_LOGW(TAG, "SSP Passkey request not handled in this example.");
break;
#endif // CONFIG_BT_SSP_ENABLED
case ESP_BT_GAP_MODE_CHG_EVT: // GAP Mode Change Event
ESP_LOGI(TAG, "GAP Mode Changed: mode: %d, BDA: %s", param->mode_chg.mode,
bda_to_str(param->mode_chg.bda, peer_bda_str, sizeof(peer_bda_str)));
break;
default: {
ESP_LOGD(TAG, "Unhandled GAP Event: %d", event);
break;
}
}
}
// A2DP Sink event callback function
// This function is called by the Bluedroid stack when A2DP Sink events occur.
static void bt_app_a2d_cb(esp_a2d_sink_cb_event_t event, esp_a2d_sink_cb_param_t *param) {
// Pass the event to the application state machine handler
bt_app_av_sm_handler(event, param);
}
// A2DP Sink audio data callback function
// This function is called when decoded PCM audio data is received from the A2DP Source.
static void bt_app_a2d_data_cb(const uint8_t *data, uint32_t len) {
if (len == 0 || data == NULL) {
return; // No data to process
}
size_t bytes_written = 0;
// Write the received PCM data to the I2S peripheral for playback
// The timeout (pdMS_TO_TICKS(100)) prevents indefinite blocking if I2S buffer is full.
esp_err_t err = i2s_write(I2S_NUM, data, len, &bytes_written, pdMS_TO_TICKS(100));
if (err != ESP_OK) {
ESP_LOGE(TAG, "I2S write failed: %s", esp_err_to_name(err));
}
if (bytes_written < len) {
// This indicates that the I2S buffer was full and not all data could be written.
// This might lead to audio glitches (underrun at the DAC).
ESP_LOGW(TAG, "I2S write underrun: wrote %u of %u bytes. Audio may be choppy.", (unsigned int)bytes_written, (unsigned int)len);
}
}
// AVRCP Target (TG) callback function
// This function handles events related to remote control commands (e.g., play, pause, volume).
static void bt_app_rc_tg_cb(esp_avrc_tg_cb_event_t event, esp_avrc_tg_cb_param_t *param) {
char peer_bda_str[18];
switch (event) {
case ESP_AVRC_TG_CONNECTION_STATE_EVT:
ESP_LOGI(TAG, "AVRCP Target Connection State: %s, BDA: %s",
param->conn_stat.connected ? "Connected" : "Disconnected",
bda_to_str(param->conn_stat.remote_bda, peer_bda_str, sizeof(peer_bda_str)));
// If disconnected, you might want to re-initialize or await new connection
break;
case ESP_AVRC_TG_PASSTHROUGH_CMD_EVT: // Received a passthrough command (e.g. play, pause)
ESP_LOGI(TAG, "AVRCP Passthrough CMD: ID 0x%x, State %s",
param->pt_cmd.key_code,
(param->pt_cmd.key_state == ESP_AVRC_PT_CMD_STATE_PRESSED) ? "Pressed" : "Released");
// Example: Handle PLAY command
if (param->pt_cmd.key_code == ESP_AVRC_PT_CMD_PLAY && param->pt_cmd.key_state == ESP_AVRC_PT_CMD_STATE_PRESSED) {
ESP_LOGI(TAG, "AVRCP PLAY command received.");
// Application logic to resume audio if it was paused locally,
// or inform the A2DP source if it's controlling the stream.
// Note: A2DP audio start/stop is often controlled by ESP_A2D_AUDIO_STATE_EVT.
}
// Add handlers for other commands like PAUSE, STOP, NEXT, PREVIOUS, VOL_UP, VOL_DOWN
break;
case ESP_AVRC_TG_SET_ABSOLUTE_VOLUME_CMD_EVT: // Received an absolute volume command
ESP_LOGI(TAG, "AVRCP Set Absolute Volume CMD: Volume %d (0-127)", param->set_abs_vol.volume);
// Here, you would typically adjust the actual audio output volume.
// This might involve software volume control on the PCM data or controlling an external amplifier.
// For this example, we just log it.
// Respond to the source that the volume command was processed.
esp_avrc_tg_send_set_absolute_volume_rsp(param->set_abs_vol.idx, param->set_abs_vol.trans_label, param->set_abs_vol.volume);
break;
case ESP_AVRC_TG_REGISTER_NOTIFICATION_EVT: // Source registered for notifications
ESP_LOGI(TAG, "AVRCP Register Notification: Event ID 0x%x, Param %d",
param->reg_ntf.event_id, param->reg_ntf.event_parameter);
// Example: Handle playback status notification registration
if (param->reg_ntf.event_id == ESP_AVRC_NID_PLAY_STATUS) {
// Send initial response for playback status
esp_avrc_playback_stat_t playback_status = (s_audio_state == ESP_A2D_AUDIO_STATE_STARTED) ? ESP_AVRC_PLAYBACK_PLAYING : ESP_AVRC_PLAYBACK_STOPPED;
esp_avrc_tg_send_register_notification_rsp(param->reg_ntf.idx, ESP_AVRC_RSP_INTERIM, param->reg_ntf.trans_label,
param->reg_ntf.event_id, &playback_status);
}
break;
// Add other AVRCP Target event handlers as needed (e.g., metadata requests)
default:
ESP_LOGD(TAG, "Unhandled AVRCP TG Event: %d", event);
break;
}
}
// Application state machine handler for A2DP and related events
static void bt_app_av_sm_handler(uint16_t event, void *param) {
ESP_LOGD(TAG, "%s: Event 0x%x, Current AV State: %d, Audio State: %d",
__func__, event, s_app_av_state, s_audio_state);
char peer_bda_str[18]; // Buffer for peer BDA string
switch (s_app_av_state) {
case APP_AV_STATE_IDLE: // Initial state, typically transitions on stack up
case APP_AV_STATE_UNCONNECTED:
if (event == ESP_A2D_SINK_INIT_EVT) { // A2DP Sink profile initialized
if (((esp_a2d_sink_cb_param_t *)(param))->init_stat.status == ESP_A2D_INIT_SUCCESS) {
ESP_LOGI(TAG, "A2DP Sink initialized successfully.");
s_app_av_state = APP_AV_STATE_UNCONNECTED;
// Already set to discoverable/connectable in app_main
} else {
ESP_LOGE(TAG, "A2DP Sink initialization failed, status: %d", ((esp_a2d_sink_cb_param_t *)(param))->init_stat.status);
}
} else if (event == ESP_A2D_CONNECTION_STATE_EVT) {
esp_a2d_sink_cb_param_t *a2d_param = (esp_a2d_sink_cb_param_t *)(param);
ESP_LOGI(TAG, "A2DP Connection State: %s, BDA: %s",
a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED ? "Connected" :
a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTING ? "Connecting" :
a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED ? "Disconnected" : "Unknown",
bda_to_str(a2d_param->conn_stat.remote_bda, peer_bda_str, sizeof(peer_bda_str)));
if (a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTING) {
s_app_av_state = APP_AV_STATE_CONNECTING;
memcpy(s_peer_bda, a2d_param->conn_stat.remote_bda, ESP_BD_ADDR_LEN);
} else if (a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
s_app_av_state = APP_AV_STATE_CONNECTED;
memcpy(s_peer_bda, a2d_param->conn_stat.remote_bda, ESP_BD_ADDR_LEN);
ESP_LOGI(TAG, "Connected to: %s. Disabling GAP discoverability.", peer_bda_str);
esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE); // Optional: save power
} else if (a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
s_app_av_state = APP_AV_STATE_UNCONNECTED;
memset(s_peer_bda, 0, ESP_BD_ADDR_LEN); // Clear peer BDA
ESP_LOGI(TAG, "Disconnected. Setting GAP to connectable and discoverable.");
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
if (s_audio_state == ESP_A2D_AUDIO_STATE_STARTED) { // If audio was playing, stop I2S
ESP_LOGI(TAG, "Audio was playing, stopping I2S due to disconnect.");
i2s_stop(I2S_NUM);
i2s_zero_dma_buffer(I2S_NUM);
s_audio_state = ESP_A2D_AUDIO_STATE_STOPPED;
}
}
}
break;
case APP_AV_STATE_CONNECTING:
if (event == ESP_A2D_CONNECTION_STATE_EVT) {
esp_a2d_sink_cb_param_t *a2d_param = (esp_a2d_sink_cb_param_t *)(param);
ESP_LOGI(TAG, "A2DP Connection State (from connecting): %s, BDA: %s",
a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED ? "Connected" : "Disconnected",
bda_to_str(a2d_param->conn_stat.remote_bda, peer_bda_str, sizeof(peer_bda_str)));
if (a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
s_app_av_state = APP_AV_STATE_CONNECTED;
ESP_LOGI(TAG, "Connected to: %s. Disabling GAP discoverability.", peer_bda_str);
esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
} else if (a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
s_app_av_state = APP_AV_STATE_UNCONNECTED;
memset(s_peer_bda, 0, ESP_BD_ADDR_LEN);
ESP_LOGI(TAG, "Connection failed. Setting GAP to connectable and discoverable.");
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
}
}
break;
case APP_AV_STATE_CONNECTED:
if (event == ESP_A2D_CONNECTION_STATE_EVT) {
esp_a2d_sink_cb_param_t *a2d_param = (esp_a2d_sink_cb_param_t *)(param);
ESP_LOGI(TAG, "A2DP Connection State (from connected): %s",
a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED ? "Disconnected" : "Other");
if (a2d_param->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
s_app_av_state = APP_AV_STATE_UNCONNECTED;
memset(s_peer_bda, 0, ESP_BD_ADDR_LEN);
ESP_LOGI(TAG, "Disconnected. Setting GAP to connectable and discoverable.");
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
if (s_audio_state == ESP_A2D_AUDIO_STATE_STARTED) {
ESP_LOGI(TAG, "Audio was playing, stopping I2S due to disconnect.");
i2s_stop(I2S_NUM);
i2s_zero_dma_buffer(I2S_NUM);
s_audio_state = ESP_A2D_AUDIO_STATE_STOPPED;
}
}
} else if (event == ESP_A2D_AUDIO_STATE_EVT) {
esp_a2d_sink_cb_param_t *a2d_param = (esp_a2d_sink_cb_param_t *)(param);
ESP_LOGI(TAG, "A2DP Audio State: %s",
a2d_param->audio_stat.state == ESP_A2D_AUDIO_STATE_STARTED ? "Started" :
a2d_param->audio_stat.state == ESP_A2D_AUDIO_STATE_STOPPED ? "Stopped" :
a2d_param->audio_stat.state == ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND ? "Remote Suspend" : "Unknown");
s_audio_state = a2d_param->audio_stat.state;
if (s_audio_state == ESP_A2D_AUDIO_STATE_STARTED) {
ESP_LOGI(TAG, "Audio streaming started. Starting I2S playback.");
i2s_start(I2S_NUM); // Start I2S DMA to play audio
} else if (s_audio_state == ESP_A2D_AUDIO_STATE_STOPPED ||
s_audio_state == ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND) {
ESP_LOGI(TAG, "Audio streaming stopped/suspended. Stopping I2S playback.");
i2s_stop(I2S_NUM);
i2s_zero_dma_buffer(I2S_NUM); // Clear any pending data in I2S buffer
}
} else if (event == ESP_A2D_AUDIO_CFG_EVT) {
esp_a2d_sink_cb_param_t *a2d_param = (esp_a2d_sink_cb_param_t *)(param);
ESP_LOGI(TAG, "A2DP Audio Configured: Codec: %s, Sample Rate: %d Hz, Channels: %d, Bits/Sample: %d",
(a2d_param->audio_cfg.mcc.type == ESP_A2D_MCT_SBC) ? "SBC" : "Other",
a2d_param->audio_cfg.mcc.sample_freq,
a2d_param->audio_cfg.mcc.channels,
a2d_param->audio_cfg.mcc.bits_per_sample);
// Stop I2S if it was running with old config
if (s_audio_state == ESP_A2D_AUDIO_STATE_STARTED) {
i2s_stop(I2S_NUM);
i2s_zero_dma_buffer(I2S_NUM);
}
// Reconfigure I2S clock based on the negotiated audio parameters
uint32_t sample_rate = a2d_param->audio_cfg.mcc.sample_freq;
uint8_t bits_per_sample_val = a2d_param->audio_cfg.mcc.bits_per_sample;
uint8_t channels_val = a2d_param->audio_cfg.mcc.channels;
// ESP-IDF A2DP sink decodes SBC to 16-bit PCM.
// The bits_per_sample from mcc might be for SBC, not PCM.
// For I2S, we typically use 16-bit.
i2s_bits_per_sample_t i2s_bps = I2S_BITS_PER_SAMPLE_16BIT;
if (bits_per_sample_val == ESP_A2D_BITS_PER_SAMPLE_24) {
// i2s_bps = I2S_BITS_PER_SAMPLE_24BIT; // If stack provides 24-bit PCM
} else if (bits_per_sample_val == ESP_A2D_BITS_PER_SAMPLE_32) {
// i2s_bps = I2S_BITS_PER_SAMPLE_32BIT; // If stack provides 32-bit PCM
}
i2s_channel_t i2s_channels = (channels_val == ESP_A2D_CHANNEL_MODE_MONO) ? I2S_CHANNEL_MONO : I2S_CHANNEL_STEREO;
esp_err_t clk_ret = i2s_set_clk(I2S_NUM, sample_rate, i2s_bps, i2s_channels);
if (clk_ret == ESP_OK) {
ESP_LOGI(TAG, "I2S clock reconfigured to %d Hz, %d BPS, %s",
(int)sample_rate, (int)i2s_bps, (i2s_channels == I2S_CHANNEL_MONO) ? "Mono" : "Stereo");
} else {
ESP_LOGE(TAG, "Failed to reconfigure I2S clock: %s", esp_err_to_name(clk_ret));
}
// If audio was playing, the source might send a START event again after reconfig.
// Or, if it was suspended, it might remain so.
// The ESP_A2D_AUDIO_STATE_EVT will handle restarting I2S if needed.
}
break;
case APP_AV_STATE_DISCONNECTING: // Not explicitly used, handled by transitions from CONNECTED
break;
default:
ESP_LOGE(TAG, "%s: Unhandled AV State %d with Event 0x%x", __func__, s_app_av_state, event);
break;
}
}
Key ESP-IDF A2DP Sink & AVRCP Target Events
Event (esp_a2d_sink_cb_event_t / esp_avrc_tg_cb_event_t) | Description | Key Parameters in param union |
---|---|---|
A2DP Sink Events (esp_a2d_sink_cb) | ||
ESP_A2D_CONNECTION_STATE_EVT | A2DP connection state has changed (e.g., connecting, connected, disconnected). |
param->conn_stat.state (e.g., ESP_A2D_CONNECTION_STATE_CONNECTED) param->conn_stat.remote_bda (BD_ADDR of the A2DP Source) |
ESP_A2D_AUDIO_STATE_EVT | A2DP audio stream state has changed (e.g., started, stopped, remote suspend). | param->audio_stat.state (e.g., ESP_A2D_AUDIO_STATE_STARTED, ESP_A2D_AUDIO_STATE_STOPPED) |
ESP_A2D_AUDIO_CFG_EVT | Audio stream configuration is received from the A2DP Source (codec, sample rate, channels). Crucial for configuring I2S. |
param->audio_cfg.mcc.type (Codec type, e.g., ESP_A2D_MCT_SBC) param->audio_cfg.mcc.sample_freq (e.g., 44100, 48000) param->audio_cfg.mcc.channels (e.g., ESP_A2D_CHANNEL_MODE_STEREO) param->audio_cfg.mcc.bits_per_sample (Bits per sample of encoded audio) |
A2DP Sink Data Callback (esp_a2d_sink_data_cb_t) | ||
(Data Callback) | Called when decoded PCM audio data is available from the A2DP stack. This data should be written to I2S. |
const uint8_t *data (Pointer to PCM data buffer) uint32_t len (Length of PCM data in bytes) |
AVRCP Target Events (bt_app_rc_tg_cb) | ||
ESP_AVRC_TG_CONNECTION_STATE_EVT | AVRCP connection state has changed. |
param->conn_stat.connected (True if connected, false otherwise) param->conn_stat.remote_bda |
ESP_AVRC_TG_PASSTHROUGH_CMD_EVT | A passthrough command (e.g., play, pause, stop, next, previous, volume up/down) is received from the A2DP Source. |
param->pt_cmd.key_code (e.g., ESP_AVRC_PT_CMD_PLAY) param->pt_cmd.key_state (e.g., ESP_AVRC_PT_CMD_STATE_PRESSED) |
ESP_AVRC_TG_SET_ABSOLUTE_VOLUME_CMD_EVT | An absolute volume command is received. The Sink should adjust its volume and can respond. | param->set_abs_vol.volume (Volume level, typically 0-127) |
ESP_AVRC_TG_REGISTER_NOTIFICATION_EVT | The A2DP Source has registered for notifications on certain events (e.g., playback status change, track change). The Sink should respond and send updates when these events occur. |
param->reg_ntf.event_id (e.g., ESP_AVRC_NID_PLAY_STATUS) param->reg_ntf.trans_label (Transaction label for response) |
3. Build Instructions
- Open a terminal in VS Code.
- Ensure your ESP-IDF environment is sourced (e.g., by running
get_idf
or opening an ESP-IDF terminal). - Clean the project (optional, but good practice for first build): Bash
idf.py fullclean
- Build the project: Bash
idf.py build
4. Run/Flash/Observe
- Connect your ESP32 board to your computer. Ensure you have an I2S DAC and amplifier/speaker connected to the ESP32 pins defined in the code (I2S_BCK_PIN, I2S_WS_PIN, I2S_DO_PIN).
- Tip: Common I2S DAC modules include MAX98357A (mono, amplified), PCM5102A (stereo, line-out), or UDA1334A (stereo, line-out). Verify their pinouts and connect them to the ESP32’s VCC (3.3V), GND, and the I2S data lines.
- Flash the firmware to the ESP32. Replace
(YOUR_SERIAL_PORT)
with your ESP32’s serial port (e.g.,/dev/ttyUSB0
on Linux,COM3
on Windows): Bashidf.py -p (YOUR_SERIAL_PORT) flash
- Open the serial monitor to observe the logs: Bash
idf.py -p (YOUR_SERIAL_PORT) monitor
- You should see logs indicating:
- NVS initialization.
- Bluetooth controller and Bluedroid stack initialization.
- GAP, A2DP Sink, and AVRCP Target callbacks registered.
- A2DP Sink profile initialized.
- Device name set and scan mode enabled.
- I2S interface initialized.
- “A2DP Sink Demo Initialized. Waiting for connections…”
- The local Bluetooth Device Address (BDA) of your ESP32.
5. Testing with an A2DP Source (e.g., Smartphone)
- On your smartphone or other A2DP source device (e.g., laptop), enable Bluetooth.
- Scan for new Bluetooth devices. You should see your ESP32 listed with the name “ESP32_A2DP_Sink” (or the name you configured).
- Select the ESP32 device from the list to initiate pairing and connection.
- If Secure Simple Pairing (SSP) is used, you might be prompted on your phone and/or see logs on the ESP32 serial monitor for a confirmation request (e.g., “GAP SSP Confirmation request… Numeric value: xxxxxx”). Confirm the pairing on your phone if prompted. The example code auto-confirms on the ESP32 side.
- Once connected, the ESP32’s serial monitor should log A2DP connection events (
ESP_A2D_CONNECTION_STATE_EVT
transitioning to connected) and possibly AVRCP connection events. - Open a music player app on your smartphone and start playing audio. Ensure the audio output on your phone is routed to the connected “ESP32_A2DP_Sink” device.
- You should hear the audio playing through the speaker connected to your ESP32’s I2S DAC.
- Observe the ESP32’s serial monitor:
ESP_A2D_AUDIO_CFG_EVT
will show the negotiated audio codec parameters (SBC, sample rate, channels).ESP_A2D_AUDIO_STATE_EVT
will change toESP_A2D_AUDIO_STATE_STARTED
when audio begins streaming.- You might see logs from
bt_app_a2d_data_cb
related toi2s_write
if there are issues, but successful writes are usually silent unless you add specific logging.
- Try controlling playback from your phone (pause, play, skip). You should see corresponding
ESP_A2D_AUDIO_STATE_EVT
changes (e.g., toESP_A2D_AUDIO_STATE_REMOTE_SUSPEND
orSTOPPED
when paused/stopped, thenSTARTED
when resumed). If your phone sends AVRCP commands, you’ll see logs frombt_app_rc_tg_cb
. - When you disconnect from your phone, the ESP32 will log the disconnection and become discoverable again.
Variant Notes
The A2DP profile is part of Bluetooth Classic (BR/EDR). Therefore, its availability on ESP32 variants depends on their Bluetooth Classic support:
ESP32 Variant | A2DP Sink Support | Underlying Bluetooth Requirement | Key Notes for A2DP Sink |
---|---|---|---|
ESP32 (Original/Classic) | Yes | Bluetooth Classic (BR/EDR) | Full support. Commonly used for A2DP Sink projects. |
ESP32-S2 | No | N/A (No Bluetooth) | No Bluetooth hardware, thus no A2DP. |
ESP32-S3 | Yes | Bluetooth Classic (BR/EDR) | Supports A2DP Sink. Performance generally good. |
ESP32-C2 | No | BLE Only | Does not support Bluetooth Classic, so A2DP Sink is not available. |
ESP32-C3 | No | BLE Only | Does not support Bluetooth Classic, so A2DP Sink is not available. |
ESP32-C6 | No* | BLE Only (+802.15.4) / Dual Mode* | *As per prior chapter context (Ch. 52), assumed BLE only for course consistency. (Note: Official ESP32-C6 specs indicate BR/EDR support, which would enable A2DP. Clarify based on course’s specific stance on C6 capabilities). For this table, adhering to prior chapter’s “No Classic” for C6. |
ESP32-H2 | No | BLE Only (+802.15.4) | Does not support Bluetooth Classic, so A2DP Sink is not available. |
In summary: For A2DP Sink development as described in this chapter, you should use the ESP32 (original) or ESP32-S3 variants. An external I2S DAC, along with an amplifier and speaker, is typically required for audio output. While some ESP32 development boards might integrate a simple DAC, an external I2S DAC generally provides better audio quality.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
A2DP/Sink Not Enabled in menuconfig |
Compilation errors (e.g., esp_a2d_sink_… undefined). Runtime: A2DP Sink profile fails to initialize (esp_a2d_sink_init() error). |
1. Run idf.py menuconfig. 2. Navigate to: Component config -> Bluetooth -> Bluedroid Options. 3. Ensure [*] A2DP and [*] A2DP Sink (SNK) are enabled. 4. Save configuration and rebuild (idf.py build). |
I2S Interface Not Configured or Incorrect Wiring |
No audio output, heavily distorted audio, or I2S-related errors in serial monitor (e.g., “I2S driver install failed”, “I2S set pin failed”). Audio plays only on one channel or is noisy. |
1. Double-check I2S pin definitions (I2S_BCK_PIN, I2S_WS_PIN, I2S_DO_PIN) in code against physical connections to the DAC. 2. Ensure DAC is correctly powered (3.3V) and has a common GND with ESP32. 3. Verify I2S driver functions (i2s_driver_install, i2s_set_pin, i2s_set_clk) succeed. Check logs. 4. Ensure I2S clock is correctly reconfigured in ESP_A2D_AUDIO_CFG_EVT based on negotiated sample rate and bit depth (typically 16-bit for PCM output from ESP-IDF). |
Callbacks Not Registered or Logic Errors |
ESP32 pairs but A2DP connection doesn’t establish, or audio doesn’t start/stop as expected. Audio data received (if logged) but not passed to I2S, or I2S not started/stopped correctly. |
1. Ensure esp_a2d_sink_register_callback() and esp_a2d_sink_register_data_callback() are called before esp_a2d_sink_init(). 2. In A2DP event callback (e.g., bt_app_a2d_cb), correctly handle ESP_A2D_AUDIO_STATE_EVT to start/stop I2S playback (i2s_start(), i2s_stop(), i2s_zero_dma_buffer()). 3. In data callback (bt_app_a2d_data_cb), ensure data and length are correctly passed to i2s_write(). Avoid blocking operations. |
Incorrect Bluetooth Stack Initialization Order |
Failures during Bluetooth init (esp_bt_controller_init, esp_bluedroid_init, etc.). Unstable Bluetooth operation, unexpected disconnects. |
Follow standard ESP-IDF BT init sequence: NVS -> BT Controller Mem Release (if needed) -> BT Controller Init/Enable -> Bluedroid Init/Enable -> GAP Callback -> A2DP Callbacks -> A2DP Init -> AVRCP Init/Callback (optional). |
Audio Choppy, Glitchy, or Stuttering |
Audio playback is inconsistent, with clicks, pops, or dropouts. May see “I2S write underrun” or similar warnings. |
1. Increase I2S DMA buffer settings (dma_buf_count, dma_buf_len in i2s_config_t). Example uses 6×1024. 2. Ensure Bluetooth stack and I2S data handling tasks have sufficient CPU time and priority. Avoid starving them with other high-intensity tasks. 3. Check Bluetooth link quality: distance, interference (WiFi, microwaves). Poor link causes packet loss, distinct from I2S buffer issues. 4. Ensure i2s_write() in the data callback has an appropriate timeout and check its return for errors or partial writes. |
Pairing Issues or Repeated Pairing Requests | Device fails to pair, or successfully pairs but then immediately disconnects or requests pairing again on next connection attempt. |
1. On the A2DP Source (phone), “unpair” or “forget” the ESP32 device and try re-pairing. 2. Ensure SSP is enabled (CONFIG_BT_SSP_ENABLED=y) for a better experience. 3. Check ESP32 serial monitor for GAP authentication events (ESP_BT_GAP_AUTH_CMPL_EVT, etc.) to debug. 4. NVS issues: If old/corrupt bonding info is stored, try nvs_flash_erase() during development (clears all NVS). |
AVRCP Remote Control Not Working |
Play/pause/skip commands from phone have no effect on ESP32 (or vice-versa if ESP32 was an AVRCP controller). Volume changes from phone not reflected. |
1. Ensure AVRCP is enabled in menuconfig (if separate option exists, usually enabled with A2DP). 2. Register AVRCP Target callback (esp_avrc_tg_register_callback()) and initialize AVRCP Target (esp_avrc_tg_init()). 3. Implement logic in the AVRCP Target callback (bt_app_rc_tg_cb) to handle events like ESP_AVRC_TG_PASSTHROUGH_CMD_EVT and ESP_AVRC_TG_SET_ABSOLUTE_VOLUME_CMD_EVT. 4. For volume, actual audio level adjustment requires software manipulation of PCM data or hardware amplifier control. |
Exercises
- LED Playback Indicator:
- Modify the A2DP Sink example to control one of the ESP32’s built-in LEDs (e.g., GPIO2 on many development boards, or any other available GPIO connected to an LED).
- The LED should:
- Be OFF when no A2DP device is connected.
- Blink slowly (e.g., once per second) when an A2DP device is connected but audio is stopped/paused.
- Be ON (solid) when an A2DP device is connected and audio is actively streaming (
ESP_A2D_AUDIO_STATE_STARTED
).
- You’ll need to initialize the GPIO pin for output and change its state within the A2DP event handling logic (e.g., in
bt_app_av_sm_handler
based onESP_A2D_CONNECTION_STATE_EVT
andESP_A2D_AUDIO_STATE_EVT
).
- Display Connected Device Name (Basic):
- If you have a simple I2C OLED display (like SSD1306) connected to your ESP32, enhance the project to show the Bluetooth Device Name of the connected A2DP Source.
- The device name is available in the
param->auth_cmpl.device_name
field of theESP_BT_GAP_AUTH_CMPL_EVT
GAP callback event, or potentially through AVRCP metadata if you were to implement that. For this exercise, focus on capturing it during or after authentication. - Store this name and display it on the OLED when an A2DP connection is active. Display “Disconnected” or “Waiting…” when no device is connected.
- Hint: You’ll need to add I2C driver initialization and an OLED display library (e.g., from ESP-IDF component registry or a third-party library).
- AVRCP Volume Control Logging:
- Extend the
bt_app_rc_tg_cb
AVRCP Target callback to more clearly log volume changes. - When
ESP_AVRC_TG_PASSTHROUGH_CMD_EVT
is received withkey_code
asESP_AVRC_PT_CMD_VOL_UP
orESP_AVRC_PT_CMD_VOL_DOWN
(andkey_state
asESP_AVRC_PT_CMD_STATE_PRESSED
), print a specific message like “Volume UP command received” or “Volume DOWN command received”. - When
ESP_AVRC_TG_SET_ABSOLUTE_VOLUME_CMD_EVT
is received, log the requested absolute volume (0-127). - Challenge: Conceptually, how would you implement a software volume control on the PCM data being sent to I2S based on these AVRCP commands? (No need to fully implement, just describe the approach, e.g., scaling PCM sample values).
- Extend the
Summary
- A2DP (Advanced Audio Distribution Profile) enables unidirectional wireless streaming of high-quality stereo or mono audio over Bluetooth Classic.
- Devices in an A2DP setup act as either a Source (transmitting audio) or a Sink (receiving and playing audio). This chapter focused on the ESP32 as an A2DP Sink.
- A2DP relies on GAVDP for framework, AVDTP for transport and signaling, and mandates the SBC codec. Other codecs like AAC or MP3 are optional.
- The ESP-IDF provides the
esp_a2dp_api.h
for implementing A2DP Sink functionality. Key functions includeesp_a2d_sink_init()
,esp_a2d_sink_register_callback()
, andesp_a2d_sink_register_data_callback()
. - The A2DP Sink callback (
esp_a2d_sink_cb_t
) handles events like connection state changes (ESP_A2D_CONNECTION_STATE_EVT
), audio stream state changes (ESP_A2D_AUDIO_STATE_EVT
), and audio configuration (ESP_A2D_AUDIO_CFG_EVT
). - The A2DP data callback (
esp_a2d_sink_data_cb_t
) receives decoded PCM audio data, which is then typically sent to an I2S peripheral for output to a DAC and speaker/headphones. - AVRCP (Audio/Video Remote Control Profile) often accompanies A2DP, allowing the Sink (as an AVRCP Target) to receive playback control commands (play, pause, volume) from the Source.
- A2DP Sink functionality is primarily available on ESP32 (original) and ESP32-S3 variants due to their Bluetooth Classic (BR/EDR) support.
- Proper initialization of the Bluetooth stack, A2DP profile, I2S interface, and careful handling of callbacks are crucial for a working A2DP Sink application.
Further Reading
- ESP-IDF A2DP API Reference: https://docs.espressif.com/projects/esp-idf/en/v5.1.3/esp32/api-reference/bluetooth/esp_a2dp.html (Replace
v5.1.3
with your specific ESP-IDF version if different, e.g.,v5.0
orstable
). - ESP-IDF Bluetooth Examples on GitHub: The primary example to study is located in your ESP-IDF installation path at
$IDF_PATH/examples/bluetooth/bluedroid/classic_bt/a2dp_sink/
. - ESP-IDF I2S Driver API Reference: https://docs.espressif.com/projects/esp-idf/en/v5.1.3/esp32/api-reference/peripherals/i2s.html
- Bluetooth SIG – Advanced Audio Distribution Profile Specification: Official A2DP specifications can be found on the Bluetooth SIG website (www.bluetooth.com -> Specifications -> Adopted Specifications). You may need to search for “Advanced Audio Distribution Profile”.
