Skip to content

AVRC Control

walinsky edited this page Nov 11, 2025 · 1 revision

AVRC Control

Overview

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

Features

Playback Control

  • Play / Pause
  • Next track
  • Previous track
  • Commands sent via passthrough mechanism

Metadata Reception

  • Track title
  • Artist name
  • Album name
  • Genre
  • Track number and total tracks
  • Track duration

Status Monitoring

  • Playback state (playing/paused/stopped)
  • Current playback position
  • Song length
  • Volume level (0-127)

Basic Usage

Controlling Playback

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


Receiving Metadata

Register Metadata Callback

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);
}

Display on OLED/LCD

#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);
}

Monitoring Playback Status

Register Playback Status Callback

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);
}

Progress Bar Example

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);
}

Volume Synchronization

Register Volume Callback

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);
}

Amplifier Control Example

// 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);
}

Getting Current Metadata

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
");
    }
}

Complete Example: Button Control + Display

#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");
}

Tips and Best Practices

Handling Missing Metadata

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");
    }
}

Debouncing Button Presses

Always add debounce delays to prevent multiple command sends:

if (button_pressed(BUTTON_NEXT)) {
    a2dpSinkHfpHf_avrc_next();
    vTaskDelay(pdMS_TO_TICKS(300));  // 300ms debounce
}

Connection Status

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();
}

Memory Considerations

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);
    }
}

Troubleshooting

Metadata Not Received

  • 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

Commands Not Working

  • Verify AVRC is connected: a2dpSinkHfpHf_is_avrc_connected()
  • Some phones have limited AVRC support
  • Check Bluetooth logs for error messages

Volume Changes Not Detected

  • Not all phones support absolute volume
  • Requires Bluetooth AVRCP 1.4 or later
  • May not work with some music apps