-
-
Notifications
You must be signed in to change notification settings - Fork 1
AVRC Control
walinsky edited this page Nov 11, 2025
·
1 revision
The Audio/Video Remote Control Profile (AVRCP) allows your ESP32 to:
- Control music playback on the connected phone (play, pause, next, previous)
- Receive track metadata (title, artist, album, genre)
- Monitor playback status (playing, paused, stopped)
- Receive volume change notifications
- Play / Pause
- Next track
- Previous track
- Commands sent via passthrough mechanism
- Track title
- Artist name
- Album name
- Genre
- Track number and total tracks
- Track duration
- Playback state (playing/paused/stopped)
- Current playback position
- Song length
- Volume level (0-127)
#include "a2dpSinkHfpHf.h"
void control_music(void) {
// Start playback
if (a2dpSinkHfpHf_avrc_play()) {
ESP_LOGI(TAG, "Play command sent");
}
// Pause playback
if (a2dpSinkHfpHf_avrc_pause()) {
ESP_LOGI(TAG, "Pause command sent");
}
// Skip to next track
if (a2dpSinkHfpHf_avrc_next()) {
ESP_LOGI(TAG, "Next track command sent");
}
// Go to previous track
if (a2dpSinkHfpHf_avrc_prev()) {
ESP_LOGI(TAG, "Previous track command sent");
}
}Note: Commands return false if AVRC is not connected.
Track metadata is delivered asynchronously via callback:
void on_metadata_update(const bt_avrc_metadata_t *metadata) {
if (!metadata || !metadata->valid) {
ESP_LOGW(TAG, "Invalid metadata received");
return;
}
ESP_LOGI(TAG, "═══════════════════════════════════");
ESP_LOGI(TAG, "🎵 Now Playing:");
ESP_LOGI(TAG, " Title: %s", metadata->title);
ESP_LOGI(TAG, " Artist: %s", metadata->artist);
ESP_LOGI(TAG, " Album: %s", metadata->album);
if (metadata->genre[0] != '\0') {
ESP_LOGI(TAG, " Genre: %s", metadata->genre);
}
if (metadata->track_num > 0) {
ESP_LOGI(TAG, " Track: %d of %d",
metadata->track_num, metadata->total_tracks);
}
if (metadata->playing_time_ms > 0) {
uint32_t seconds = metadata->playing_time_ms / 1000;
uint32_t minutes = seconds / 60;
seconds %= 60;
ESP_LOGI(TAG, " Length: %d:%02d", minutes, seconds);
}
ESP_LOGI(TAG, "═══════════════════════════════════");
}
void app_main(void) {
// ... initialization ...
// Register callback
a2dpSinkHfpHf_register_avrc_metadata_callback(on_metadata_update);
}#include "ssd1306.h" // Example OLED library
void display_metadata(const bt_avrc_metadata_t *metadata) {
if (!metadata || !metadata->valid) return;
ssd1306_clear_screen();
// Title
ssd1306_draw_string(0, 0, metadata->title, 12, 1);
// Artist
ssd1306_draw_string(0, 16, metadata->artist, 10, 1);
// Album
char album_info[64];
snprintf(album_info, sizeof(album_info), "Album: %s", metadata->album);
ssd1306_draw_string(0, 32, album_info, 8, 1);
// Track number
if (metadata->track_num > 0) {
char track_info[32];
snprintf(track_info, sizeof(track_info),
"Track %d/%d", metadata->track_num, metadata->total_tracks);
ssd1306_draw_string(0, 48, track_info, 8, 1);
}
ssd1306_refresh();
}
void on_metadata_update(const bt_avrc_metadata_t *metadata) {
display_metadata(metadata);
}void on_playback_status_change(const bt_avrc_playback_status_t *status) {
const char *status_str;
switch (status->status) {
case ESP_AVRC_PLAYBACK_STOPPED:
status_str = "⏹️ Stopped";
break;
case ESP_AVRC_PLAYBACK_PLAYING:
status_str = "▶️ Playing";
break;
case ESP_AVRC_PLAYBACK_PAUSED:
status_str = "⏸️ Paused";
break;
case ESP_AVRC_PLAYBACK_FWD_SEEK:
status_str = "⏩ Forward Seek";
break;
case ESP_AVRC_PLAYBACK_REV_SEEK:
status_str = "⏪ Reverse Seek";
break;
default:
status_str = "❓ Unknown";
break;
}
ESP_LOGI(TAG, "Playback Status: %s", status_str);
// Display position
if (status->song_len_ms > 0) {
uint32_t pos_sec = status->song_pos_ms / 1000;
uint32_t len_sec = status->song_len_ms / 1000;
ESP_LOGI(TAG, "Position: %d:%02d / %d:%02d",
pos_sec / 60, pos_sec % 60,
len_sec / 60, len_sec % 60);
}
}
void app_main(void) {
// ... initialization ...
a2dpSinkHfpHf_register_avrc_playback_callback(on_playback_status_change);
}void update_progress_bar(const bt_avrc_playback_status_t *status) {
if (status->song_len_ms == 0) return;
// Calculate progress percentage
uint8_t progress = (status->song_pos_ms * 100) / status->song_len_ms;
// Draw progress bar on display
draw_progress_bar(progress);
// Time display
char time_str[32];
snprintf(time_str, sizeof(time_str), "%d:%02d / %d:%02d",
(status->song_pos_ms / 1000) / 60,
(status->song_pos_ms / 1000) % 60,
(status->song_len_ms / 1000) / 60,
(status->song_len_ms / 1000) % 60);
display_text(time_str);
}void on_volume_change(uint8_t volume) {
// Volume range: 0-127
// Convert to percentage
uint8_t percent = (volume * 100) / 127;
ESP_LOGI(TAG, "🔊 Volume: %d%% (raw: %d)", percent, volume);
// Control external amplifier via I2C/SPI
set_amplifier_volume(volume);
// Update volume indicator on display
update_volume_indicator(percent);
}
void app_main(void) {
// ... initialization ...
a2dpSinkHfpHf_register_avrc_volume_callback(on_volume_change);
}// Example with MAX9744 I2C amplifier
#include "driver/i2c.h"
#define MAX9744_I2C_ADDR 0x4B
void set_amplifier_volume(uint8_t bt_volume) {
// Convert BT volume (0-127) to MAX9744 volume (0-63)
uint8_t amp_volume = (bt_volume * 63) / 127;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MAX9744_I2C_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, amp_volume, true);
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, pdMS_TO_TICKS(1000));
i2c_cmd_link_delete(cmd);
}You can query the current metadata at any time:
void display_current_track(void) {
const bt_avrc_metadata_t *metadata = a2dpSinkHfpHf_get_avrc_metadata();
if (metadata && metadata->valid) {
printf("Current Track:
");
printf(" %s - %s
", metadata->artist, metadata->title);
} else {
printf("No track information available
");
}
}#include "a2dpSinkHfpHf.h"
#include "driver/gpio.h"
#define BUTTON_PLAY GPIO_NUM_0
#define BUTTON_NEXT GPIO_NUM_4
#define BUTTON_PREV GPIO_NUM_5
static bool is_playing = false;
void on_playback_status_change(const bt_avrc_playback_status_t *status) {
is_playing = (status->status == ESP_AVRC_PLAYBACK_PLAYING);
}
void on_metadata_update(const bt_avrc_metadata_t *metadata) {
if (metadata && metadata->valid) {
printf("\n🎵 %s - %s\n", metadata->artist, metadata->title);
}
}
void button_task(void *pvParameters) {
// Configure buttons with pull-up
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL << BUTTON_PLAY) |
(1ULL << BUTTON_NEXT) |
(1ULL << BUTTON_PREV),
.pull_up_en = GPIO_PULLUP_ENABLE,
};
gpio_config(&io_conf);
while (1) {
// Play/Pause button
if (gpio_get_level(BUTTON_PLAY) == 0) {
if (is_playing) {
a2dpSinkHfpHf_avrc_pause();
} else {
a2dpSinkHfpHf_avrc_play();
}
vTaskDelay(pdMS_TO_TICKS(300)); // Debounce
}
// Next button
if (gpio_get_level(BUTTON_NEXT) == 0) {
a2dpSinkHfpHf_avrc_next();
vTaskDelay(pdMS_TO_TICKS(300));
}
// Previous button
if (gpio_get_level(BUTTON_PREV) == 0) {
a2dpSinkHfpHf_avrc_prev();
vTaskDelay(pdMS_TO_TICKS(300));
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}
void app_main(void) {
// Initialize NVS
nvs_flash_init();
// Configure component
a2dpSinkHfpHf_config_t config = {
.device_name = "ESP32 Music Player",
.i2s_tx_bck = GPIO_NUM_26,
.i2s_tx_ws = GPIO_NUM_25,
.i2s_tx_dout = GPIO_NUM_22,
.i2s_rx_bck = GPIO_NUM_32,
.i2s_rx_ws = GPIO_NUM_33,
.i2s_rx_din = GPIO_NUM_34
};
// Register callbacks
a2dpSinkHfpHf_register_avrc_metadata_callback(on_metadata_update);
a2dpSinkHfpHf_register_avrc_playback_callback(on_playback_status_change);
// Initialize
a2dpSinkHfpHf_init(&config);
// Start button handling task
xTaskCreate(button_task, "button_task", 2048, NULL, 5, NULL);
ESP_LOGI(TAG, "Music player ready - use buttons to control playback");
}Not all music apps provide complete metadata. Always check the valid flag:
void on_metadata_update(const bt_avrc_metadata_t *metadata) {
if (!metadata || !metadata->valid) {
display_text("No track info");
return;
}
// Check individual fields
if (metadata->title[0] != '\0') {
display_title(metadata->title);
} else {
display_title("Unknown Title");
}
}Always add debounce delays to prevent multiple command sends:
if (button_pressed(BUTTON_NEXT)) {
a2dpSinkHfpHf_avrc_next();
vTaskDelay(pdMS_TO_TICKS(300)); // 300ms debounce
}Check AVRC connection before sending commands:
void send_play_command(void) {
if (!a2dpSinkHfpHf_is_avrc_connected()) {
ESP_LOGW(TAG, "AVRC not connected");
return;
}
a2dpSinkHfpHf_avrc_play();
}Metadata strings can be large (128 bytes for title/artist/album). If displaying on a small screen, truncate:
void display_truncated(const char *text, int max_len) {
char buffer[32];
if (strlen(text) > max_len) {
strncpy(buffer, text, max_len - 3);
buffer[max_len - 3] = '\0';
strcat(buffer, "...");
display_text(buffer);
} else {
display_text(text);
}
}- Some music apps don't send metadata (e.g., YouTube Music)
- Try switching to Spotify, Apple Music, or Google Play Music
- Check logs for AVRC connection status
- Verify AVRC is connected:
a2dpSinkHfpHf_is_avrc_connected() - Some phones have limited AVRC support
- Check Bluetooth logs for error messages
- Not all phones support absolute volume
- Requires Bluetooth AVRCP 1.4 or later
- May not work with some music apps