Page 1 of 1

Best Practice for Keeping A2DP Source Pipeline "Hot" Between Tracks in ESP-ADF v2.7

Posted: Tue Oct 07, 2025 2:38 am
by cofiemarkā„¢
Hello everyone,

I'm working on an ESP32 project and would appreciate some expert advice on the best way to handle the
audio pipeline between playback sessions.

Project Goal:
The device acts as a Bluetooth A2DP audio source. It uses an RFID reader to scan tags. When a tag is
scanned, it looks up a corresponding MP3 file on an SD card and streams it to a connected Bluetooth
speaker.

The desired behavior is:
1. A track plays when a card is scanned.
2. When the track finishes, the device should wait for the next card scan.
3. Crucially, the Bluetooth A2DP connection should remain active and "hot" during this idle period, so
that when the next card is scanned, the new track starts playing almost instantly without any
noticeable delay, reconnection, or audio glitches.

Environment:
* Chip: ESP32
* ESP-IDF Version: v5.3.3
* ESP-ADF Version: v2.7


The Problem:
After a track finishes playing, the A2DP media stream is automatically suspended by the framework.
While the Bluetooth link itself doesn't always drop immediately, this suspension seems to cause
issues with reliably starting the next track. When the next RFID scan triggers a new playback
request, there is often a significant delay, and sometimes the playback fails to start, with the
following error appearing in the logs:

Code: Select all

E (35725) BT_APPL: bta_dm_pm_btm_status hci_status=36
This suggests the Bluetooth controller is entering a power-saving or incorrect state that it cannot
gracefully recover from when the pipeline is run again.

Current Implementation:
My application uses a standard ESP-ADF audio_pipeline with the following elements:
fatfs_stream_reader -> mp3_decoder -> a2dp_stream_writer

Here are the relevant code snippets from my audio_ctrl.c file:

1. Pipeline Initialization (`audio_init`)

Code: Select all

    1 void audio_init(void)
    2 {
    3     // ... bluetooth and other initializations ...
    4 
    5     audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
    6     pipeline = audio_pipeline_init(&pipeline_cfg);
    7 
    8     fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT();
    9     fatfs_stream_reader = fatfs_stream_init(&fatfs_cfg);
   10 
   11     mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG();
   12     mp3_decoder = mp3_decoder_init(&mp3_cfg);
   13 
   14     a2dp_stream_config_t a2dp_cfg = {
   15         .type = AUDIO_STREAM_WRITER,
   16     };
   17     a2dp_stream_writer = a2dp_stream_init(&a2dp_cfg);
   18 
   19     audio_pipeline_register(pipeline, fatfs_stream_reader, "file");
   20     audio_pipeline_register(pipeline, mp3_decoder, "mp3");
   21     audio_pipeline_register(pipeline, a2dp_stream_writer, "a2dp");
   22 
   23     audio_pipeline_link(pipeline, (const char *[]) {"file", "mp3", "a2dp"}, 3);
   24 
   25     // ... event listener setup ...
   26 }
2. Event Listener (Current State)
To prevent the entire pipeline from terminating, I have modified the event listener to simply update
the application's internal state when a track finishes, instead of calling audio_pipeline_stop().

Code: Select all

    1 static esp_err_t audio_event_iface_listener(audio_event_iface_msg_t *event, void *context)
    2 {
    3     if (event->source_type == AUDIO_ELEMENT_TYPE_ELEMENT) {
    4         if (event->source == (void *)mp3_decoder) {
    5             if (event->cmd == AEL_MSG_CMD_REPORT_STATUS && (int)event->data ==
      AEL_STATUS_STATE_FINISHED) {
    6                 ESP_LOGI(TAG, "Playback finished. Setting state to IDLE.");
    7                 xSemaphoreTake(g_player_state_mutex, portMAX_DELAY);
    8                 g_player_state = AUDIO_STATE_IDLE;
    9                 xSemaphoreGive(g_player_state_mutex);
   10             }
   11         }
   12     }
   13     return ESP_OK;
   14 }
3. Playback Request Function (`audio_request_play`)
This function is called by the RFID task.

Code: Select all

    1 void audio_request_play(const char *filepath)
    2 {
    3     xSemaphoreTake(g_player_state_mutex, portMAX_DELAY);
    4 
    5     if (g_player_state == AUDIO_STATE_PLAYING) {
    6         ESP_LOGW(TAG, "Stopping current track to play new one.");
    7         _audio_stop_play_nolock(); // This calls audio_pipeline_stop()
    8     }
    9 
   10     ESP_LOGI(TAG, "Setting URI to: %s", filepath);
   11     audio_element_set_uri(fatfs_stream_reader, filepath);
   12 
   13     audio_pipeline_reset_ringbuffer(pipeline);
   14     audio_pipeline_reset_elements(pipeline);
   15 
   16     ESP_LOGI(TAG, "Starting pipeline.");
   17     if (audio_pipeline_run(pipeline) == ESP_OK) {
   18         g_player_state = AUDIO_STATE_PLAYING;
   19     } else {
   20         ESP_LOGE(TAG, "Failed to start pipeline");
   21         g_player_state = AUDIO_STATE_IDLE;
   22     }
   23 
   24     xSemaphoreGive(g_player_state_mutex);
   25 }
Relevant Log Output:
Here is the log showing a track finishing and the subsequent Bluetooth error.

Code: Select all

    1 W (28305) FATFS_STREAM: No more data, ret:0
    2 I (28305) AUDIO_ELEMENT: IN-[file] AEL_IO_DONE,0
    3 I (28305) AUDIO_ELEMENT: IN-[mp3] AEL_IO_DONE,-2
    4 I (28455) AUDIO_ELEMENT: IN-[a2dp] AEL_IO_DONE,-2
    5 W (28455) BT_LOG: ESP_A2D_MEDIA_CTRL_STOP is deprecated, using ESP_A2D_MEDIA_CTRL_SUSPEND
      instead.
    6 
    7 W (28465) BT_APPL: ### UNDERFLOW :: ONLY READ -2 BYTES OUT OF 512 ###
    8 W (28495) BT_APPL: ### UNDERFLOW :: ONLY READ -2 BYTES OUT OF 514 ###
    9 W (28515) BT_APPL: Media task Scheduled after Suspend
   10 I (28515) MP3_DECODER: Closed
   11 I (28525) audio_ctrl: Playback finished. Setting state to IDLE.
   12 
   13 ... (idle period) ...
   14 
   15 W (35715) BT_HCI: hci cmd send: sniff: hdl 0x80, intv(400 800)
   16 W (35725) BT_HCI: hcif mode change: hdl 0x80, mode 0, intv 0, status 0x24
   17 E (35725) BT_APPL: bta_dm_pm_btm_status hci_status=36
As you can see, the a2dp_stream element suspends the media task itself, which seems to lead to the
power management issue later on.

The Core Question:
What is the recommended best practice in ESP-ADF v2.7 to keep an A2DP source pipeline "hot" and
responsive between tracks? The goal is to prevent the A2DP media session from suspending so that the
next call to audio_pipeline_run() is fast and reliable.

Is there a better way to handle the pipeline state transitions for this specific use case (one-shot
playback with a need for instant readiness)?

Thank you in advance for any guidance you can provide