Chapter 54: Bluetooth A2DP Source Implementation
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the Advanced Audio Distribution Profile (A2DP) Source role and its responsibilities.
- Explain how to acquire or generate audio data (PCM) on the ESP32 for streaming.
- Describe the process of audio encoding (e.g., PCM to SBC) as handled by the A2DP Source stack.
- Initialize and configure the A2DP Source module in ESP-IDF v5.x.
- Implement an A2DP Source on an ESP32 to stream audio to a compatible A2DP Sink device.
- Handle A2DP connection, audio state, and data request events from a Source perspective.
- Test A2DP audio streaming from the ESP32 to a standard Bluetooth speaker or headphones.
- Identify ESP32 variants that support the A2DP Source role.
- Troubleshoot common issues encountered in A2DP Source implementations.
Introduction
In the previous chapter, we explored how the ESP32 can act as an A2DP Sink, receiving and playing audio streamed from devices like smartphones. Now, we flip the roles and investigate how to implement an A2DP Source on the ESP32. As an A2DP Source, the ESP32 will be responsible for originating the audio stream and transmitting it wirelessly to an A2DP Sink (e.g., Bluetooth headphones, speakers, or another ESP32 configured as a Sink).
This capability opens up various applications for the ESP32, such as:
- Creating custom audio announcement systems.
- Building simple wireless music players that stream audio from an SD card or internal flash memory.
- Transmitting audio from a microphone connected to the ESP32, effectively creating a wireless microphone.
- Generating and streaming audio alerts or sound effects in IoT projects.
This chapter will guide you through the theoretical aspects of the A2DP Source role and provide a practical example of implementing an A2DP Source on the ESP32. We will generate a simple audio tone (a sine wave) on the ESP32 and stream it to a connected Bluetooth audio device.
Theory
A2DP Source Role Revisited
As discussed in Chapter 53, A2DP defines two primary roles:
- Source (SRC): The device that originates and transmits the audio stream.
- Sink (SNK): The device that receives and renders the audio stream.
In this chapter, our ESP32 will take on the Source (SRC) role. This means it will be responsible for:
- Acquiring or Generating Audio: The audio data must come from somewhere. This could be a microphone, a file read from storage, or programmatically generated audio (like a tone). This raw audio is typically in Pulse Code Modulation (PCM) format.
- Encoding Audio: The raw PCM audio data is usually too large for efficient wireless transmission. The A2DP Source uses an audio codec to compress the PCM data. The SBC (Low Complexity Subband Codec) is mandatory for all A2DP devices. The ESP-IDF’s A2DP Source stack handles the SBC encoding internally when provided with PCM data.
- Managing the A2DP Connection: The Source initiates the connection to a Sink, negotiates stream parameters (like codec type and settings), and manages the audio stream lifecycle (start, stop, suspend).
- Transporting Audio Data: The encoded audio data is packetized using AVDTP (Audio/Video Distribution Transport Protocol) and sent over an L2CAP channel to the A2DP Sink.
Key Protocols from the Source Perspective
The A2DP Source interacts with the same underlying Bluetooth protocols as the Sink, but its role in the interactions differs:
- Generic Audio/Video Distribution Profile (GAVDP): Still provides the framework. The Source uses GAVDP procedures to discover Sink capabilities and manage stream endpoints.
- Audio/Video Distribution Transport Protocol (AVDTP):
- Signaling: The Source typically initiates the AVDTP signaling connection. It discovers the Sink’s stream endpoints and their capabilities (e.g., supported codecs, sampling rates). The Source then proposes a stream configuration (e.g., “let’s use SBC at 44.1kHz stereo”) using the
SET_CONFIGURATION
command. If the Sink accepts, the Source proceeds to open the stream (OPEN
command) and then start it (START
command). - Transport: Once the stream is started, the Source sends AVDTP media packets containing the encoded audio data (e.g., SBC frames) to the Sink over the established AVDTP transport channel.
- Signaling: The Source typically initiates the AVDTP signaling connection. It discovers the Sink’s stream endpoints and their capabilities (e.g., supported codecs, sampling rates). The Source then proposes a stream configuration (e.g., “let’s use SBC at 44.1kHz stereo”) using the
- Audio Codecs (SBC Encoding): The ESP32 A2DP Source stack includes an SBC encoder. When the application provides PCM audio data, the stack encodes it into SBC format according to the negotiated parameters before transmitting it. The application generally doesn’t need to perform SBC encoding itself.
- Service Discovery Protocol (SDP): While a Sink advertises its services via SDP, a Source might use SDP to discover A2DP Sink services on remote devices if their addresses are unknown or if it needs to verify service availability. However, for simpler scenarios, the Source might connect to a known (e.g., previously paired) Sink’s address directly.
A2DP Connection Process (Source Perspective)
sequenceDiagram; %% A2DP Source Connection Flow Diagram actor Source as "A2DP Source (e.g., ESP32)"; actor Sink as "A2DP Sink (e.g., Bluetooth Speaker)"; participant Source; participant Sink; opt Discovery (if Sink BD_ADDR unknown or to verify service) Source->>Source: Start Inquiry (Optional); Source-->>Sink: Inquiry Packets (Optional); Sink-->>Source: FHS Packet (BD_ADDR, CoD) (Optional); Source->>Sink: SDP Query (for A2DP Sink UUID: 0x110B) (Optional); activate Sink; Sink-->>Source: SDP Response (Sink capabilities); deactivate Sink; end Source->>Sink: 1. Paging & Baseband Connection (ACL Link to known/discovered BD_ADDR); activate Sink; Sink-->>Source: Connection Established; deactivate Sink; rect rgb(219, 234, 254) /* Light Blue for Process Nodes */ Source->>Sink: 2. 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: 3. AVDTP: DISCOVER (Find Sink's Stream End Points - SEPs); activate Sink; Sink-->>Source: AVDTP: DISCOVER Response (List of SEPs); deactivate Sink; Source->>Sink: 4. AVDTP: GET_CAPABILITIES (For a chosen SEP); activate Sink; Sink-->>Source: AVDTP: GET_CAPABILITIES Response (Supported codecs, etc.); deactivate Sink; Source->>Sink: 5. 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: 6. 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: 7. AVDTP: START (Request to start audio stream); activate Sink; Sink-->>Source: AVDTP: Stream Started Acknowledgment; deactivate Sink; loop Audio Data Streaming Source->>Source: Acquire/Generate PCM Audio Data; Source->>Source: Provide PCM Data to A2DP Stack (via callback); Note right of Source: Stack encodes PCM to SBC; Source-->>Sink: AVDTP Audio Packets (Encoded SBC frames); end end opt Stream Control (Pause/Stop/Abort) Source->>Sink: AVDTP: SUSPEND / CLOSE / ABORT; activate Sink; Sink-->>Source: AVDTP: Acknowledgment; deactivate Sink; end Note over Source,Sink: Connection may also involve AVRCP Controller (Source) sending commands to AVRCP Target (Sink).
- (Optional) Discovery: If the Sink’s Bluetooth Device Address (BD_ADDR) is unknown, the Source may perform an Inquiry to find nearby devices and then an SDP query on a candidate device to check for A2DP Sink service (UUID 0x110B) and its capabilities.
- Baseband Connection: The Source initiates a Bluetooth baseband (ACL) connection to the target Sink device using its BD_ADDR.
- AVDTP Signaling Connection: The Source establishes an L2CAP connection for AVDTP signaling with the Sink.
- Discover and Configure Stream (AVDTP DISCOVER, GET_CAPABILITIES, SET_CONFIGURATION):
- The Source sends an AVDTP
DISCOVER
command to find the Sink’s Stream End Points (SEPs). - For each relevant SEP, the Source sends
GET_CAPABILITIES
to learn about supported codecs and parameters. - The Source chooses a compatible configuration (e.g., SBC, 44.1kHz, stereo) and proposes it to the Sink using
SET_CONFIGURATION
. The Sink responds, accepting or rejecting the configuration.
- The Source sends an AVDTP
- Open Stream (AVDTP OPEN): Once a configuration is accepted, the Source sends an
OPEN
command to prepare the AVDTP transport channel. - Start Stream (AVDTP START): The Source sends a
START
command to the Sink. Upon acknowledgment, the Source can begin sending audio data. - Audio Data Transmission: The Source application provides PCM data. The A2DP stack encodes this data (e.g., to SBC), packetizes it into AVDTP media packets, and transmits them to the Sink.
- Stream Control (AVDTP SUSPEND, CLOSE, ABORT): The Source can suspend, close, or abort the stream. The Sink might also request suspension.
Audio Data Flow (Source Side)
- Audio Acquisition/Generation: The application running on the ESP32 generates or acquires raw audio data in PCM format. This could be from:
- An I2S microphone.
- A data buffer containing a pre-loaded sound.
- Algorithmic generation (e.g., sine wave, noise).
- A file from an SD card or SPIFFS (requiring a decoder for formats like WAV or MP3 into PCM).
- PCM Data Buffering: The application typically buffers this PCM data.
- Data Provision to A2DP Stack: The ESP-IDF A2DP Source stack needs to receive this PCM data. This is usually handled via a callback mechanism: the application registers a data callback function. When the A2DP stack is ready to send more audio, it invokes this callback. The callback function’s responsibility is to provide the next chunk of PCM data to the stack.
- Alternatively, some implementations might use a ring buffer where the application writes PCM data, and the A2DP stack’s data request callback reads from this ring buffer. The
esp_a2d_source_data_write()
function can be used in conjunction with such a scheme.
- Alternatively, some implementations might use a ring buffer where the application writes PCM data, and the A2DP stack’s data request callback reads from this ring buffer. The
- SBC Encoding: The A2DP stack takes the provided PCM data and encodes it into SBC frames using the parameters negotiated with the Sink.
- AVDTP Packetization and Transmission: The SBC frames are wrapped in AVDTP media packets and sent over the L2CAP transport channel to the Sink.
graph TD; %% A2DP Audio Data Flow Diagram (Source Side) %% Styles classDef appLogic fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef buffer fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef stack fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef transport fill:#E0E7FF,stroke:#3730A3,stroke-width:1px,color:#3730A3; classDef remote fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46; subgraph ESP32_Application_A2DP_Source ["ESP32 Application (A2DP Source)"] direction TB APP_GEN["Audio Generation/Acquisition<br>(e.g., Sine Wave Task, I2S Mic Read, File Read)"]:::appLogic; APP_PCM_DATA["Raw PCM Audio Data<br>(e.g., 16-bit, 44.1kHz, Stereo)"]:::appLogic; APP_RINGBUF["Ring Buffer<br>(Optional, for decoupling tasks)"]:::buffer; end subgraph ESP_IDF_Bluetooth_Stack ["ESP-IDF Bluetooth Stack (A2DP Source)"] direction TB STACK_DATA_CB["Data Callback<br>(esp_a2d_source_data_cb_t)"]:::stack; STACK_SBC_ENC["SBC Encoder"]:::stack; STACK_AVDTP_PACKET["AVDTP Packetizer"]:::stack; end subgraph Bluetooth_Wireless_Transmission ["Bluetooth Wireless Transmission"] direction TB L2CAP["L2CAP Channel"]:::transport; RADIO["Bluetooth Radio (PHY)"]:::transport; end A2DP_SINK["A2DP Sink Device<br>(e.g., Bluetooth Speaker)"]:::remote; APP_GEN --> APP_PCM_DATA; APP_PCM_DATA --> APP_RINGBUF; APP_RINGBUF -- "Provides PCM Data" --> STACK_DATA_CB; STACK_DATA_CB -- "PCM Data" --> STACK_SBC_ENC; STACK_SBC_ENC -- "SBC Encoded Frames" --> STACK_AVDTP_PACKET; STACK_AVDTP_PACKET -- "AVDTP Media Packets" --> L2CAP; L2CAP --> RADIO; RADIO -- "RF Signal" --> A2DP_SINK; %% Alternative if not using ring buffer, direct provision from a source %% APP_PCM_DATA -- "Provides PCM Data directly or via simple buffer" --> STACK_DATA_CB;
Audio/Video Remote Control Profile (AVRCP) – Controller Role
When an ESP32 acts as an A2DP Source, it often also implements the AVRCP Controller (CT) role. This allows the ESP32 to send playback control commands (e.g., play, pause, stop, next track, previous track, volume control) to the A2DP Sink, assuming the Sink supports the AVRCP Target (TG) role. While our main example will focus on A2DP Source, AVRCP CT is a common companion for a complete audio streaming solution.
Practical Examples
Example 1: ESP32 as an A2DP Source Streaming a Generated Sine Wave
This example demonstrates how to configure the ESP32 to act as an A2DP Source, generate a sine wave, and stream it to a connected A2DP Sink (e.g., a Bluetooth speaker or headphones). This approach simplifies audio acquisition, allowing us to focus on the A2DP Source mechanics.
1. Project Setup (VS Code with ESP-IDF Extension)
- Create a new ESP-IDF project in VS Code (e.g.,
a2dp_source_demo
). - Create or open the
sdkconfig.defaults
file in your project’s root directory and add/ensure 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_SOURCE_ENABLE=y
CONFIG_BT_SSP_ENABLED=y
CONFIG_BT_CTRL_MODE_EFF=2 # Classic Bluetooth Mode
CONFIG_BT_BLUEDROID_NAME="ESP32_A2DP_Source"
# Optional: For SBC encoder quality/bitrate settings. Default may be fine.
# CONFIG_A2DP_SBC_ENC_QUALITY_HIGH=y
# CONFIG_A2DP_SBC_ENC_DYNAMIC_BITPOOL_ALLOC_ENABLED=y
# CONFIG_A2DP_SBC_ENC_DYNAMIC_BITPOOL_ALLOC_MAX_BITRATE=328 # Example value, kbps
- 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 Source (SRC)
(Ensure Sink is disabled if not used)Component config
->Bluetooth
->Bluedroid Options
->[*] Secure Simple Pairing
Component config
->Bluetooth
->Bluetooth controller
->Bluetooth controller mode (CLASSIC_BT)
- Set
(ESP32_A2DP_Source)
underComponent config
->Bluetooth
->Bluedroid Options
->Default Bluedroid Host name
.
2. C Code Implementation (main/a2dp_source_main.c
)
The following code implements the A2DP Source. It generates a sine wave and uses a ring buffer to pass data to the A2DP stack’s data request callback.
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include <math.h> // For sin()
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/ringbuf.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 Controller (optional)
static const char *TAG = "A2DP_SOURCE_DEMO";
// BD_ADDR of the A2DP Sink device to connect to (e.g., Bluetooth speaker)
// Replace with your Sink's BD_ADDR.
// Example: {0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
// You can find this by scanning with another device or from the Sink's pairing info.
// For testing, you might need to discover it first or use a known paired device.
static esp_bd_addr_t s_peer_bda = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}; // TODO: REPLACE THIS!
// Audio properties for generated sine wave
#define SINE_WAVE_SAMPLE_RATE 44100
#define SINE_WAVE_CHANNELS 2 // Stereo
#define SINE_WAVE_BITS_PER_SAMPLE 16
#define SINE_WAVE_FREQUENCY 440 // A4 note
#define SINE_WAVE_AMPLITUDE (INT16_MAX / 2) // Amplitude for 16-bit signed PCM
// Ring buffer for audio data
#define RINGBUFFER_SIZE (8 * 1024) // Bytes
static RingbufHandle_t s_audio_ringbuf = NULL;
static TaskHandle_t s_audio_gen_task_handle = NULL;
// A2DP application state machine
typedef enum {
APP_AV_STATE_IDLE,
APP_AV_STATE_DISCOVERING,
APP_AV_STATE_DISCOVERED,
APP_AV_STATE_UNCONNECTED,
APP_AV_STATE_CONNECTING,
APP_AV_STATE_CONNECTED,
APP_AV_STATE_DISCONNECTING,
} app_av_state_t;
// Forward declarations
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_cb_event_t event, esp_a2d_cb_param_t *param);
static int32_t bt_app_a2d_data_cb(uint8_t *data, int32_t len); // Application provides data
static void audio_generator_task(void *arg);
static void bt_app_av_sm_handler(uint16_t event, void *param);
// Global state
static esp_a2d_connection_state_t s_conn_state = ESP_A2D_CONNECTION_STATE_DISCONNECTED;
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 char s_local_bda_str[18];
// 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;
}
void app_main(void) {
esp_err_t ret;
// --- Initialize NVS, Bluetooth Controller, Bluedroid Stack ---
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.");
ESP_LOGI(TAG, "Releasing BT controller memory for BLE mode (if any)...");
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
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.");
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.");
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.");
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 Callbacks and Initialize A2DP Source ---
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, "Registering A2DP Source event callback...");
ret = esp_a2d_register_callback(bt_app_a2d_cb); // Common callback for Source & Sink events
if (ret != ESP_OK) {
ESP_LOGE(TAG, "A2DP event callback register failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "Registering A2DP Source data provider callback...");
// This callback is invoked by the stack when it needs audio data from the application.
ret = esp_a2d_source_register_data_callback(bt_app_a2d_data_cb);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "A2DP Source data callback register failed: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "Initializing A2DP Source profile...");
ret = esp_a2d_source_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "A2DP Source init failed: %s", esp_err_to_name(ret));
return;
}
s_app_av_state = APP_AV_STATE_IDLE; // Initial state update after successful init
// --- Set Device Name and Scan Mode ---
const char * configured_device_name;
esp_bt_dev_get_device_name(&configured_device_name);
ESP_LOGI(TAG, "Device name is '%s' (from menuconfig).", configured_device_name);
// ESP32 A2DP Source usually initiates connection, so discoverability might not be critical
// But setting it can be useful for other profiles or debugging.
ESP_LOGI(TAG, "Setting device to connectable and discoverable (optional for source)...");
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
// --- Create Audio Ring Buffer and Generator Task ---
s_audio_ringbuf = xRingbufferCreate(RINGBUFFER_SIZE, RINGBUF_TYPE_BYTEBUF);
if (s_audio_ringbuf == NULL) {
ESP_LOGE(TAG, "Failed to create audio ring buffer.");
return;
}
ESP_LOGI(TAG, "Audio ring buffer created.");
xTaskCreate(audio_generator_task, "AudioGenTask", 4096, NULL, configMAX_PRIORITIES - 3, &s_audio_gen_task_handle);
ESP_LOGI(TAG, "Audio generator task created.");
ESP_LOGI(TAG, "A2DP Source Demo Initialized. Will attempt to connect to Sink BDA: %s",
bda_to_str(s_peer_bda, s_local_bda_str, sizeof(s_local_bda_str))); // Using s_local_bda_str as temp buffer for peer BDA
// 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.");
}
// Automatically try to connect to the hardcoded peer BDA
// Ensure s_peer_bda is correctly set above!
// A check if s_peer_bda is all zeros might be good here.
bool is_peer_bda_set = false;
for(int i=0; i<ESP_BD_ADDR_LEN; ++i) {
if(s_peer_bda[i] != 0x00) {
is_peer_bda_set = true;
break;
}
}
if (!is_peer_bda_set) {
ESP_LOGE(TAG, "Peer BDA is not set! Please update s_peer_bda in the code.");
ESP_LOGW(TAG, "You can find the BDA of your speaker/headphones by pairing them with your phone/PC and checking the device details.");
} else {
ESP_LOGI(TAG, "Attempting to connect to A2DP Sink: %s", bda_to_str(s_peer_bda, (char[18]){0}, 18));
esp_a2d_source_connect(s_peer_bda);
s_app_av_state = APP_AV_STATE_CONNECTING;
}
}
// Audio generator task: produces a sine wave and writes it to the ring buffer
static void audio_generator_task(void *arg) {
uint32_t pcm_data_size = SINE_WAVE_CHANNELS * (SINE_WAVE_BITS_PER_SAMPLE / 8);
int16_t *pcm_buf = malloc(1024 * pcm_data_size); // Buffer for ~1024 stereo samples
if (!pcm_buf) {
ESP_LOGE(TAG, "Failed to allocate PCM buffer for audio generator task.");
vTaskDelete(NULL);
return;
}
double phase = 0.0;
double phase_step = 2.0 * M_PI * SINE_WAVE_FREQUENCY / SINE_WAVE_SAMPLE_RATE;
ESP_LOGI(TAG, "Audio generator task started. Producing %d Hz sine wave at %d kHz, %d-bit, %s.",
(int)SINE_WAVE_FREQUENCY, SINE_WAVE_SAMPLE_RATE / 1000, SINE_WAVE_BITS_PER_SAMPLE,
SINE_WAVE_CHANNELS == 2 ? "stereo" : "mono");
while (true) {
if (s_audio_state == ESP_A2D_AUDIO_STATE_STARTED) {
// Generate 1024 samples
for (int i = 0; i < 1024; ++i) {
int16_t sample_val = (int16_t)(SINE_WAVE_AMPLITUDE * sin(phase));
if (SINE_WAVE_CHANNELS == 2) {
pcm_buf[i * 2] = sample_val; // Left channel
pcm_buf[i * 2 + 1] = sample_val; // Right channel (same for simplicity)
} else {
pcm_buf[i] = sample_val; // Mono channel
}
phase += phase_step;
if (phase >= 2.0 * M_PI) {
phase -= 2.0 * M_PI;
}
}
// Send data to ring buffer
// xRingbufferSend will block if buffer is full, up to a timeout (portMAX_DELAY here)
UBaseType_t res = xRingbufferSend(s_audio_ringbuf, pcm_buf, 1024 * pcm_data_size, pdMS_TO_TICKS(100));
if (res != pdTRUE) {
ESP_LOGW(TAG, "Audio ring buffer send timeout or fail.");
}
} else {
// Audio not started, wait a bit to avoid busy-looping
vTaskDelay(pdMS_TO_TICKS(100));
}
}
free(pcm_buf); // Should not be reached in this loop
vTaskDelete(NULL);
}
// Bluetooth GAP callback
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];
switch (event) {
case ESP_BT_GAP_AUTH_CMPL_EVT:
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)));
} 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)));
// If auth fails during connection attempt, reset state
if (s_app_av_state == APP_AV_STATE_CONNECTING && memcmp(param->auth_cmpl.bda, s_peer_bda, ESP_BD_ADDR_LEN) == 0) {
s_app_av_state = APP_AV_STATE_UNCONNECTED;
}
}
break;
#if (CONFIG_BT_SSP_ENABLED == true)
case ESP_BT_GAP_CFM_REQ_EVT:
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);
esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true); // Auto-confirm
break;
// Other SSP events like KEY_NOTIF, KEY_REQ can be handled if needed
#endif
default:
ESP_LOGD(TAG, "Unhandled GAP Event: %d", event);
break;
}
}
// A2DP Source event callback (common for Source and Sink)
static void bt_app_a2d_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t *param) {
// Pass to state machine handler
bt_app_av_sm_handler(event, param);
}
// A2DP Source data provider callback
// This function is called by the Bluedroid stack when it needs audio data.
// It should provide PCM data. The length `len` is the max bytes the stack can accept.
// Return value should be the number of bytes actually provided.
static int32_t bt_app_a2d_data_cb(uint8_t *data, int32_t len) {
if (s_audio_ringbuf == NULL || len == 0 || data == NULL) {
return 0;
}
size_t item_size = 0;
// Attempt to receive data from the ring buffer
// Non-blocking: 0 ticks timeout. If data is not immediately available, don't wait.
uint8_t *buffer_item = (uint8_t *)xRingbufferReceiveUpTo(s_audio_ringbuf, &item_size, 0, len);
if (buffer_item != NULL && item_size > 0) {
memcpy(data, buffer_item, item_size);
// Return the received item to the ring buffer
vRingbufferReturnItem(s_audio_ringbuf, (void *)buffer_item);
return item_size;
}
// No data available or error
return 0;
}
// Application state machine handler for A2DP Source
static void bt_app_av_sm_handler(uint16_t event, void *param) {
ESP_LOGD(TAG, "%s: Event 0x%x, Current AV State: %d, Conn State: %d, Audio State: %d",
__func__, event, s_app_av_state, s_conn_state, s_audio_state);
char peer_bda_str[18];
switch (event) {
case ESP_A2D_CONNECTION_STATE_EVT: {
esp_a2d_cb_param_t *a2d_param = (esp_a2d_cb_param_t *)(param);
s_conn_state = a2d_param->conn_stat.state; // Update global connection state
bda_to_str(a2d_param->conn_stat.remote_bda, peer_bda_str, sizeof(peer_bda_str));
ESP_LOGI(TAG, "A2DP Connection State: %s, BDA: %s",
s_conn_state == ESP_A2D_CONNECTION_STATE_DISCONNECTED ? "Disconnected" :
s_conn_state == ESP_A2D_CONNECTION_STATE_CONNECTING ? "Connecting" :
s_conn_state == ESP_A2D_CONNECTION_STATE_CONNECTED ? "Connected" : "Unknown",
peer_bda_str);
if (s_conn_state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
s_app_av_state = APP_AV_STATE_CONNECTED;
ESP_LOGI(TAG, "Connected to Sink. Ready to start audio stream.");
// Optionally, auto-start the stream after connection
// esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_START); // Or wait for user action
} else if (s_conn_state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
s_app_av_state = APP_AV_STATE_UNCONNECTED;
s_audio_state = ESP_A2D_AUDIO_STATE_STOPPED; // Ensure audio state is reset
ESP_LOGI(TAG, "Disconnected from Sink.");
// Re-attempt connection or wait for user action
// For simplicity, this example doesn't auto-reconnect here.
} else if (s_conn_state == ESP_A2D_CONNECTION_STATE_CONNECTING) {
s_app_av_state = APP_AV_STATE_CONNECTING;
}
break;
}
case ESP_A2D_AUDIO_STATE_EVT: {
esp_a2d_cb_param_t *a2d_param = (esp_a2d_cb_param_t *)(param);
s_audio_state = a2d_param->audio_stat.state; // Update global audio state
ESP_LOGI(TAG, "A2DP Audio State: %s",
s_audio_state == ESP_A2D_AUDIO_STATE_STARTED ? "Started" :
s_audio_state == ESP_A2D_AUDIO_STATE_STOPPED ? "Stopped" :
s_audio_state == ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND ? "Remote Suspend" : "Unknown");
if (s_audio_state == ESP_A2D_AUDIO_STATE_STARTED) {
ESP_LOGI(TAG, "Audio stream started. Generator task will now actively send data.");
// Audio generator task will see s_audio_state and start pushing to ringbuffer
} else if (s_audio_state == ESP_A2D_AUDIO_STATE_STOPPED || s_audio_state == ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND) {
ESP_LOGI(TAG, "Audio stream stopped or suspended. Generator task will pause sending.");
// Clear ring buffer if needed, or let it drain if stream might resume.
// For simplicity, we just let the generator pause.
// If stopped and not planning to resume soon, could clear ringbuffer:
// if (s_audio_ringbuf) { xRingbufferReset(s_audio_ringbuf); }
}
break;
}
case ESP_A2D_AUDIO_CFG_EVT: { // Audio stream configuration event
esp_a2d_cb_param_t *a2d_param = (esp_a2d_cb_param_t *)(param);
ESP_LOGI(TAG, "A2DP Audio Configured by Sink: 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);
// This confirms the parameters the Sink has accepted.
// Our generated sine wave uses fixed parameters, so we hope they match or are compatible.
// The A2DP stack handles the negotiation.
// If we are connected and configured, we can start the media stream.
if (s_conn_state == ESP_A2D_CONNECTION_STATE_CONNECTED && s_audio_state == ESP_A2D_AUDIO_STATE_STOPPED) {
ESP_LOGI(TAG, "Audio configured. Sending MEDIA_CTRL_START to begin streaming.");
esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_START);
}
break;
}
case ESP_A2D_MEDIA_CTRL_ACK_EVT: { // Acknowledgement for media control command
esp_a2d_cb_param_t *a2d_param = (esp_a2d_cb_param_t *)(param);
ESP_LOGI(TAG, "A2DP Media Control ACK: Command %s, Status %s",
a2d_param->media_ctrl_stat.cmd == ESP_A2D_MEDIA_CTRL_START ? "START" :
a2d_param->media_ctrl_stat.cmd == ESP_A2D_MEDIA_CTRL_STOP ? "STOP" : "UNKNOWN_CMD",
a2d_param->media_ctrl_stat.status == ESP_A2D_MEDIA_CTRL_ACK_SUCCESS ? "SUCCESS" : "FAILURE");
if (a2d_param->media_ctrl_stat.cmd == ESP_A2D_MEDIA_CTRL_START &&
a2d_param->media_ctrl_stat.status == ESP_A2D_MEDIA_CTRL_ACK_SUCCESS) {
ESP_LOGI(TAG, "Media START acknowledged by Sink. Audio should begin playing on Sink.");
// Audio state will change via ESP_A2D_AUDIO_STATE_EVT
} else if (a2d_param->media_ctrl_stat.cmd == ESP_A2D_MEDIA_CTRL_STOP &&
a2d_param->media_ctrl_stat.status == ESP_A2D_MEDIA_CTRL_ACK_SUCCESS) {
ESP_LOGI(TAG, "Media STOP acknowledged by Sink.");
s_audio_state = ESP_A2D_AUDIO_STATE_STOPPED; // Manually update as audio state event might be delayed
}
break;
}
// Other events like ESP_A2D_PROF_STATE_EVT can be handled if needed for A2DP source deinit.
default:
ESP_LOGD(TAG, "Unhandled A2DP Event: 0x%x", event);
break;
}
}
Key ESP-IDF A2DP Source & AVRCP Controller Events
Event (esp_a2d_cb_event_t / esp_avrc_ct_cb_event_t) | Description | Key Parameters in param union |
---|---|---|
A2DP Source Events (esp_a2d_cb) | ||
ESP_A2D_SOURCE_INIT_EVT (Implicit via esp_a2d_cb with esp_a2d_source_init) | A2DP Source profile initialization status. (Note: ESP-IDF uses a common esp_a2d_cb_event_t, context implies source role here). The param->init_stat may be used. | param->init_stat.status (Success/failure) |
ESP_A2D_CONNECTION_STATE_EVT | A2DP connection state has changed (e.g., connecting, connected, disconnected) with a Sink. |
param->conn_stat.state (e.g., ESP_A2D_CONNECTION_STATE_CONNECTED) param->conn_stat.remote_bda (BD_ADDR of the A2DP Sink) |
ESP_A2D_AUDIO_STATE_EVT | A2DP audio stream state has changed (e.g., started, stopped, remote suspend by Sink). | 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 confirmed/negotiated with the Sink (codec, sample rate, channels). |
param->audio_cfg.mcc.type (Codec type, e.g., ESP_A2D_MCT_SBC) param->audio_cfg.mcc.sample_freq param->audio_cfg.mcc.channels param->audio_cfg.mcc.bits_per_sample (of encoded audio) |
ESP_A2D_MEDIA_CTRL_ACK_EVT | Acknowledgement received from Sink for a media control command sent by Source (e.g., START, STOP). |
param->media_ctrl_stat.cmd (e.g., ESP_A2D_MEDIA_CTRL_START) param->media_ctrl_stat.status (e.g., ESP_A2D_MEDIA_CTRL_ACK_SUCCESS) |
A2DP Source Data Callback (esp_a2d_source_data_cb_t) | ||
(Data Callback Function) | Called by the A2DP stack when it needs PCM audio data to encode and send. Application must provide data. |
uint8_t *data (Buffer to fill with PCM data) int32_t len (Max length of data stack can accept in this call) Return Value: Number of bytes actually written to data buffer. |
AVRCP Controller Events (esp_avrc_ct_cb_event_t) – Optional for A2DP Source | ||
ESP_AVRC_CT_CONNECTION_STATE_EVT | AVRCP Controller connection state with Sink (AVRCP Target) has changed. |
param->conn_stat.connected param->conn_stat.remote_bda |
ESP_AVRC_CT_PASSTHROUGH_RSP_EVT | Response received from Sink for a passthrough command sent by ESP32 (e.g., response to PLAY/PAUSE). |
param->psth_rsp.key_code param->psth_rsp.key_state param->psth_rsp.rsp_code (AVRCP response code) |
ESP_AVRC_CT_REMOTE_FEATURES_EVT | Remote AVRCP Target features received. | param->rmt_feats.feat_mask, param->rmt_feats.remote_bda |
3. Build Instructions
- Open a terminal in VS Code.
- Ensure your ESP-IDF environment is sourced.
- Crucially, update
s_peer_bda
ina2dp_source_main.c
with the actual Bluetooth Device Address of your A2DP Sink device (e.g., Bluetooth speaker, headphones). You can usually find this by pairing the Sink with your phone/PC and checking its Bluetooth details, or by using a Bluetooth scanner app. - Clean the project (optional):
idf.py fullclean
- Build the project:
idf.py build
4. Run/Flash/Observe
- Connect your ESP32 board to your computer.
- Flash the firmware. Replace
(YOUR_SERIAL_PORT)
:idf.py -p (YOUR_SERIAL_PORT) flash
- Open the serial monitor:
idf.py -p (YOUR_SERIAL_PORT) monitor
- On your A2DP Sink device (speaker/headphones):
- Ensure it’s powered on and in pairing mode or discoverable mode if it’s not already paired with the ESP32.
- If it was previously paired with the ESP32 under a different profile or name, it might be good to “forget” the ESP32 on the Sink device first.
- Observe ESP32 Logs:
- You should see logs for NVS, Bluetooth stack initialization.
- The ESP32 will log its local BDA and the target Sink BDA.
- It will attempt to connect: “Attempting to connect to A2DP Sink: XX:XX:XX:XX:XX:XX”.
- Logs for GAP authentication (SSP confirmation may appear if it’s the first pairing).
- A2DP connection state changes:
Connecting
->Connected
. - A2DP audio configuration event, showing the negotiated parameters (SBC, sample rate, etc.).
- Media control ACK for START.
- A2DP audio state changing to
Started
. - The “Audio generator task started…” message.
- Listen to your A2DP Sink: Once the logs indicate
A2DP Audio State: Started
and “Media START acknowledged”, you should hear the generated 440Hz sine wave playing on your Bluetooth speaker/headphones. - To stop, you can disconnect the Sink from its interface, or reset the ESP32. The example doesn’t include an explicit stop mechanism via a button, but this could be added as an exercise.
Variant Notes
The A2DP Source profile, like the Sink profile, is part of Bluetooth Classic (BR/EDR). Its availability on ESP32 variants is determined by their Bluetooth Classic support:
ESP32 Variant | A2DP Source Support | Underlying Bluetooth Requirement | Key Notes for A2DP Source |
---|---|---|---|
ESP32 (Original/Classic) | Yes | Bluetooth Classic (BR/EDR) | Full support. Suitable for A2DP Source applications. |
ESP32-S2 | No | N/A (No Bluetooth) | No Bluetooth hardware, thus no A2DP. |
ESP32-S3 | Yes | Bluetooth Classic (BR/EDR) | Supports A2DP Source. SBC encoding performance is generally good. |
ESP32-C2 | No | BLE Only | Does not support Bluetooth Classic, so A2DP Source is not available. |
ESP32-C3 | No | BLE Only | Does not support Bluetooth Classic, so A2DP Source is not available. |
ESP32-C6 | No* | BLE Only (+802.15.4) / Dual Mode* | *As per prior chapter context (Ch. 52 & 53), assumed BLE only for course consistency. (Note: Official ESP32-C6 specs indicate BR/EDR support. If course adopts this, A2DP Source would be possible. Adhering to “No Classic” for C6 for this course unless specified otherwise). |
ESP32-H2 | No | BLE Only (+802.15.4) | Does not support Bluetooth Classic, so A2DP Source is not available. |
In summary: For A2DP Source development, the ESP32 (original) or ESP32-S3 are the recommended variants. The computational load for SBC encoding is handled by these chips, but for more complex audio processing or higher quality codecs (if supported by custom extensions), performance limitations might need consideration.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Incorrect or Missing Sink BD_ADDR |
ESP32 fails to connect to the A2DP Sink. Logs show connection timeouts or errors from esp_a2d_source_connect(). No ESP_A2D_CONNECTION_STATE_EVT to connected state. |
1. Verify the s_peer_bda variable in your code matches the exact BD_ADDR of your target A2DP Sink (speaker/headphones). 2. Ensure the Sink device is powered on, discoverable, or in pairing mode when the ESP32 attempts connection. 3. If the Sink was previously paired, try “forgetting” the ESP32 on the Sink and re-pair. |
A2DP Source Not Enabled in menuconfig |
Compilation errors (e.g., esp_a2d_source_… undefined). Runtime: A2DP Source profile fails to initialize (esp_a2d_source_init() error). |
1. Run idf.py menuconfig. 2. Navigate to: Component config -> Bluetooth -> Bluedroid Options. 3. Ensure [*] A2DP and [*] A2DP Source (SRC) are enabled. 4. Save configuration and rebuild (idf.py build). |
Audio Data Callback (bt_app_a2d_data_cb) Issues |
No audio, choppy/distorted audio on the Sink. ESP32 logs might show warnings about data underruns or the callback not providing enough data. Callback returns 0 frequently or blocks. |
1. Ensure the callback copies PCM data into the uint8_t *data buffer provided by the stack, up to the int32_t len specified. 2. The callback must return the actual number of bytes written into data. 3. Avoid lengthy operations or blocking calls within this callback. Use a ring buffer (as in example) to decouple data generation from the stack’s request. 4. Verify PCM data format (sample rate, bits/sample, channels) provided by your app matches what the A2DP stack expects for encoding (typically 16-bit stereo/mono PCM). |
Pairing and Authentication Failures |
Connection fails during pairing/authentication. Repeated pairing prompts on Sink or ESP32 logs authentication errors in GAP callback. |
1. On the A2DP Sink, “forget” or “unpair” the ESP32 and re-initiate pairing by making the Sink discoverable when ESP32 connects. 2. Ensure SSP (CONFIG_BT_SSP_ENABLED=y) is enabled for smoother pairing. The example auto-confirms SSP. 3. For persistent issues, try nvs_flash_erase() on ESP32 (clears bonding keys), then re-pair. |
Task Starvation for Audio Generation | Choppy audio, glitches, or silence because the audio generation task (e.g., audio_generator_task) cannot fill the audio buffer in time. |
1. Ensure the audio generation task has sufficient priority (e.g., configMAX_PRIORITIES – 3). 2. If using a ring buffer, ensure its size is adequate for buffering. 3. Profile CPU usage if other tasks are running to ensure the audio pipeline isn’t delayed. Ensure the audio task yields appropriately (e.g., vTaskDelay) if it’s waiting for conditions. |
Media Control Issues (START/STOP) |
Audio stream doesn’t start on Sink after connection, or doesn’t stop when commanded. ESP_A2D_MEDIA_CTRL_ACK_EVT shows failure or is not received. |
1. Ensure esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_START) is called after A2DP connection is established and audio is configured (ESP_A2D_AUDIO_CFG_EVT). 2. Check the status in ESP_A2D_MEDIA_CTRL_ACK_EVT. If it’s a failure, the Sink might have rejected the command. 3. Ensure the Sink is ready and not in an error state. Some Sinks require user interaction to accept incoming streams. |
Exercises
- Manual Connection Trigger:
- Modify the A2DP Source example so that it doesn’t automatically connect on startup.
- Add a GPIO button. When the button is pressed, the ESP32 should then attempt to connect to the
s_peer_bda
and start streaming. - A second press (or another button) could initiate
esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_STOP)
and thenesp_a2d_source_disconnect(s_peer_bda)
.
- Stream WAV File Data (Conceptual + Basic Implementation):
- Conceptual: Research how to read a simple WAV file (e.g., 16-bit PCM, 44.1kHz, stereo) from SPIFFS on the ESP32.
- Basic Implementation:
- Add SPIFFS support to your project.
- Include a small, simple WAV file in your SPIFFS image. (You’ll need to learn how to create a SPIFFS image and embed it).
- Modify the
audio_generator_task
(or create a new task) to:- Open and read PCM data from the WAV file (skipping the WAV header).
- Feed this PCM data into the
s_audio_ringbuf
instead of the sine wave. - Handle looping the WAV file or stopping when it ends.
- Tip: This is a more involved exercise. Start by just logging PCM data from the WAV file to confirm reading, then integrate with A2DP.
- Basic AVRCP Controller: Send Play/Pause:
- Initialize AVRCP Controller profile:
esp_avrc_ct_init()
and register its callbackesp_avrc_ct_register_callback()
. - After a successful A2DP connection (
ESP_A2D_CONNECTION_STATE_EVT
to connected), useesp_avrc_ct_send_passthrough_cmd()
to send a “PLAY” command to the connected Sink. (Theidx
parameter foresp_avrc_ct_send_passthrough_cmd
can often be obtained from the AVRCP connection event or by iterating connected devices). - Add a GPIO button. When pressed, toggle between sending “PAUSE” and “PLAY” passthrough commands to the Sink.
- Log the responses in the AVRCP Controller callback.
- Initialize AVRCP Controller profile:
Summary
- The A2DP Source role involves the ESP32 generating/acquiring audio, encoding it (typically to SBC via the stack), and streaming it to an A2DP Sink.
- ESP-IDF provides
esp_a2d_source_init()
,esp_a2d_register_callback()
, andesp_a2d_source_register_data_callback()
for A2DP Source implementation. - The application provides PCM audio data to the A2DP stack via the registered data callback when the stack requests it.
- Connection to a Sink is initiated by the Source using
esp_a2d_source_connect()
. Media streaming is controlled usingesp_a2d_media_ctrl()
. - Key events include connection state changes, audio state changes (started, stopped), audio configuration, and media control acknowledgments.
- A2DP Source is supported on ESP32 (original) and ESP32-S3 variants.
- Careful management of audio data provision, Sink BD_ADDR, and pairing is crucial for successful A2DP Source operation.
- AVRCP Controller functionality often complements A2DP Source for playback control.
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 (Adjust version number as needed).
- ESP-IDF AVRCP API Reference: https://docs.espressif.com/projects/esp-idf/en/v5.1.3/esp32/api-reference/bluetooth/esp_avrc.html
- ESP-IDF Bluetooth Examples on GitHub:
$IDF_PATH/examples/bluetooth/bluedroid/classic_bt/a2dp_source/
in your ESP-IDF installation. - WAV file format: For Exercise 2, understanding the basic structure of a PCM WAV file header will be necessary to extract raw audio data.
