Skip to content

feature: bt classic a2dp sink external codec api bindings - #660

Open
billylindeman wants to merge 7 commits into
esp-rs:masterfrom
billylindeman:billy/a2dp-external-codec-api
Open

feature: bt classic a2dp sink external codec api bindings#660
billylindeman wants to merge 7 commits into
esp-rs:masterfrom
billylindeman:billy/a2dp-external-codec-api

Conversation

@billylindeman

@billylindeman billylindeman commented May 15, 2026

Copy link
Copy Markdown

Submission Checklist 📝

  • I have updated existing examples or added new ones (if applicable).
  • I have used cargo fmt command to ensure that all changed code is formatted correctly.
  • I have used cargo clippy command to ensure that all changed code passes latest Clippy nightly lints.
  • My changes were added to the CHANGELOG.md in the proper section.

Pull Request Details 📖

Description

The latest build of esp-idf supports a new bt classic a2dp sink mode for external codec integration. It has a new api surface, that plumbs the encoded audio to the application for decoding using your own codecs (or esp_audio_codec). I have added support for this api, and I have added an example that shows pairing a2dp with an iphone using AAC codec params, and a callback triggers with the encoded aac data.

I do have this working with https://github.com/billylindeman/esp-audio-codec-rs decoding AAC from my iphone on a project I'm working on

Testing

To run the example:

MCU=esp32 ESP_IDF_VERSION=master ESP_IDF_SDKCONFIG_DEFAULTS=.github/configs/sdkconfig.a2dp_external_codec cargo +esp espflash flash --target xtensa-esp32-espidf --example bt_a2dp_sink_external_codec --monitor
Crystal frequency: 40 MHz
Flash size:        8MB
Features:          WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None

I (1421) main_task: Started on CPU0
I (1421) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (1431) main_task: Calling app_main()
I (1431) bt_a2dp_sink_external_codec::example: step 1: logger up
I (1441) bt_a2dp_sink_external_codec::example: step 2: peripherals taken
I (1461) bt_a2dp_sink_external_codec::example: step 3: nvs taken
I (1471) bt_a2dp_sink_external_codec::example: step 4: reduce_bt_memory ok
I (1471) esp_idf_svc::bt: Init bluetooth controller
I (1471) BTDM_INIT: BT controller compile version [003f89d]
I (1481) BTDM_INIT: Using main XTAL as clock source
I (1491) esp_idf_svc::bt: Enable bluetooth controller
I (2021) esp_idf_svc::bt: Init bluedroid
I (2031) esp_idf_svc::bt: Enable bluedroid
I (2101) bt_a2dp_sink_external_codec::example: step 5: BtDriver up
I (2101) bt_a2dp_sink_external_codec::example: step 6: gap created
I (2111) bt_a2dp_sink_external_codec::example: step 7: gap configured
W (2121) BT_BTC: A2DP Enable without AVRC
I (2131) esp_idf_svc::bt::a2dp: Got event { Initialized }
I (2131) bt_a2dp_sink_external_codec::example: step 8: a2dp external-codec sink up
I (2131) bt_a2dp_sink_external_codec::example: step 9: subscribed; registering SEPs
I (2141) bt_a2dp_sink_external_codec::example: registering AAC at seid 0
I (2151) esp_idf_svc::bt::a2dp: Got event { SinkEndpointRegistered {
    seid: 0,
    state: Success,
} }
I (2151) bt_a2dp_sink_external_codec::example: SEP 0 register result: Success
I (2161) bt_a2dp_sink_external_codec::example: registering SBC at seid 1
I (2171) esp_idf_svc::bt::a2dp: Got event { SinkEndpointRegistered {
    seid: 1,
    state: Success,
} }
I (2181) bt_a2dp_sink_external_codec::example: SEP 1 register result: Success
I (2181) bt_a2dp_sink_external_codec::example: step 10: A2DP sink ready; pair with phone and play audio
W (13191) BT_HCI: hcif conn complete: hdl 0x80, st 0x0
W (13291) BT_HCI: hcif link supv_to changed: hdl 0x80, supv_to 8000
W (13471) BT_HCI: hcif link supv_to changed: hdl 0x80, supv_to 32000
W (13731) BT_HCI: hcif link supv_to changed: hdl 0x80, supv_to 8000
I (13751) esp_idf_svc::bt::a2dp: Got event { ConnectionState {
    status: Connecting,
    disconnect_abnormal: false,
} }
W (13751) BT_L2CAP: L2CAP - rcvd conn req for unknown PSM: 23
I (13771) bt_a2dp_sink_external_codec::example: a2dp event: ConnectionState { bd_addr: Status: Connecting, disconnect_abnormal: false }
W (14061) BT_RFCOMM: rfc_find_lcid_mcb LCID reused LCID:0x41 current:0x0
W (14061) BT_RFCOMM: RFCOMM_DisconnectInd LCID:0x41
I (14071) esp_idf_svc::bt::a2dp: Got event { AudioCodecConfigured {
    ),
    codec: Mpeg2_4(
        [
            128,
            1,
            4,
            131,
            232,
            0,
        ],
    ) / bitrate: Some(44100), stereo: Some(true),
} }
I (14101) bt_a2dp_sink_external_codec::example: negotiated codec with: Mpeg2_4([128, 1, 4, 131, 232, 0]) / bitrate: Some(44100), stereo: Some(true)
I (14121) esp_idf_svc::bt::a2dp: Got event { SinkServiceCapabilitiesConfigured(
    1,
) }
I (14131) bt_a2dp_sink_external_codec::example: a2dp event: SinkServiceCapabilitiesConfigured(1)
W (14431) BT_APPL: new conn_srvc id:19, app_id:0
I (14431) esp_idf_svc::bt::a2dp: Got event { ConnectionState {
    bd_addr: 
    status: Connected,
    disconnect_abnormal: false,
} }
I (14451) bt_a2dp_sink_external_codec::example: a2dp event: ConnectionState { bd_addr:), status: Connected, disconnect_abnormal: false }
W (14461) BT_BTC: AVRC not Init, not using it.
W (17631) BT_HCI: hci cmd send: sniff: hdl 0x80, intv(400 800)
W (17691) BT_HCI: hcif mode change: hdl 0x80, mode 2, intv 800, status 0x0
W (17931) BT_HCI: hcif mode change: hdl 0x80, mode 0, intv 0, status 0x0
I (17951) BT_LOG: bta_av_link_role_ok hndl:x41 role:1 conn_audio:x1 bits:1 features:x8400

W (17951) BT_APPL: new conn_srvc id:19, app_id:1
I (17961) esp_idf_svc::bt::a2dp: Got event { AudioState {
    bd_addr:
    ),
    status: Started,
} }
I (17971) bt_a2dp_sink_external_codec::example: a2dp event: AudioState { bd_addr), status: Started }
I (18011) bt_a2dp_sink_external_codec::example: audio frame: 1 frames, 32 bytes, ts 43004047
I (18041) bt_a2dp_sink_external_codec::example: audio frame: 1 frames, 32 bytes, ts 43004069
I (18061) bt_a2dp_sink_external_codec::example: audio frame: 1 frames, 714 bytes, ts 43004093
I (18081) bt_a2dp_sink_external_codec::example: audio frame: 1 frames, 676 bytes, ts 43004116

@billylindeman billylindeman changed the title bluetooth classic a2dp sink external codec api bindings feature: bt classic a2dp sink external codec api bindings May 15, 2026

@ivmarkov ivmarkov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very good.
I have a couple of nits but we can also merge as is.
Marking as approved so you can either merge-as is or address the nits - up to you.

Comment thread Cargo.toml Outdated
Comment thread src/bt.rs
Comment thread src/bt/a2dp.rs Outdated
Comment thread src/bt/a2dp.rs

@ivmarkov ivmarkov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry - one is not nit - the pcnt-legacy thing. This you need to remove. All others are optional.

@billylindeman

billylindeman commented May 17, 2026

Copy link
Copy Markdown
Author

Ah sorry - one is not nit - the pcnt-legacy thing. This you need to remove. All others are optional.

@ivmarkov

I see the issue, if we set ESP_IDF_VERSION=master this build fails even on esp-idf-svc@master I think this change is required for the bindings to compile on v6.1 overall, not sure how you want to handle that? Just leave it up to the user of the crate?

esp-idf-svc git:(master) MCU=esp32 ESP_IDF_VERSION=master ESP_IDF_SDKCONFIG_DEFAULTS=.github/configs/sdkconfig.defaults cargo +esp espflash flash --target xtensa-esp32-espidf --example bt_ble_gap_scanner --monitor
error: could not compile `esp-idf-hal` (lib) due to 5 previous errors
error[E0422]: cannot find struct, variant or union type `pcnt_unit_config_t__bindgen_ty_1` in this scope
     --> /home/billy/.cargo/git/checkouts/esp-idf-hal-29c73b2ac8ab537b/24e99b8/src/pcnt.rs:192:24
      |
  192 |                 flags: pcnt_unit_config_t__bindgen_ty_1 {
      |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      |
     ::: /home/billy/Development/esp/esp-idf-svc/target/xtensa-esp32-espidf/debug/build/esp-idf-sys-ef8bf2065ee11924/out/bindings.rs:88903:1
      |
88903 | pub union parlio_tx_unit_config_t__bindgen_ty_1 {
      | ----------------------------------------------- similarly named union `parlio_tx_unit_config_t__bindgen_ty_1` defined here
      |
help: a union with a similar name exists
      |
  192 -                 flags: pcnt_unit_config_t__bindgen_ty_1 {
  192 +                 flags: parlio_tx_unit_config_t__bindgen_ty_1 {
      |

error[E0422]: cannot find struct, variant or union type `pcnt_chan_config_t__bindgen_ty_1` in this scope
     --> /home/billy/.cargo/git/checkouts/esp-idf-hal-29c73b2ac8ab537b/24e99b8/src/pcnt.rs:375:20
      |
  375 |             flags: pcnt_chan_config_t__bindgen_ty_1 {
      |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      |
     ::: /home/billy/Development/esp/esp-idf-svc/target/xtensa-esp32-espidf/debug/build/esp-idf-sys-ef8bf2065ee11924/out/bindings.rs:82166:1
      |
82166 | pub union i2s_chan_config_t__bindgen_ty_1 {
      | ----------------------------------------- similarly named union `i2s_chan_config_t__bindgen_ty_1` defined here
      |
help: a union with a similar name exists
      |
  375 -             flags: pcnt_chan_config_t__bindgen_ty_1 {
  375 +             flags: i2s_chan_config_t__bindgen_ty_1 {
      |

error[E0433]: cannot find module or crate `pcnt_unit_config_t__bindgen_ty_1` in this scope
   --> /home/billy/.cargo/git/checkouts/esp-idf-hal-29c73b2ac8ab537b/24e99b8/src/pcnt.rs:193:34
    |
193 |                     _bitfield_1: pcnt_unit_config_t__bindgen_ty_1::new_bitfield_1(
    |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of unresolved module or unlinked crate `pcnt_unit_config_t__bindgen_ty_1`
    |
    = help: if you wanted to use a crate named `pcnt_unit_config_t__bindgen_ty_1`, use `cargo add pcnt_unit_config_t__bindgen_ty_1` to add it to your `Cargo.toml`
help: a union with a similar name exists
    |
193 -                     _bitfield_1: pcnt_unit_config_t__bindgen_ty_1::new_bitfield_1(
193 +                     _bitfield_1: parlio_tx_unit_config_t__bindgen_ty_1::new_bitfield_1(
    |

error[E0063]: missing field `group_id` in initializer of `esp_idf_sys::pcnt_unit_config_t`
   --> /home/billy/.cargo/git/checkouts/esp-idf-hal-29c73b2ac8ab537b/24e99b8/src/pcnt.rs:183:13
    |
183 |             pcnt_unit_config_t {
    |             ^^^^^^^^^^^^^^^^^^ missing `group_id`

error[E0433]: cannot find module or crate `pcnt_chan_config_t__bindgen_ty_1` in this scope
   --> /home/billy/.cargo/git/checkouts/esp-idf-hal-29c73b2ac8ab537b/24e99b8/src/pcnt.rs:376:30
    |
376 |                 _bitfield_1: pcnt_chan_config_t__bindgen_ty_1::new_bitfield_1(
    |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of unresolved module or unlinked crate `pcnt_chan_config_t__bindgen_ty_1`
    |
    = help: if you wanted to use a crate named `pcnt_chan_config_t__bindgen_ty_1`, use `cargo add pcnt_chan_config_t__bindgen_ty_1` to add it to your `Cargo.toml`
help: a union with a similar name exists
    |
376 -                 _bitfield_1: pcnt_chan_config_t__bindgen_ty_1::new_bitfield_1(
376 +                 _bitfield_1: i2s_chan_config_t__bindgen_ty_1::new_bitfield_1(
    |

Some errors have detailed explanations: E0063, E0422, E0433.
For more information about an error, try `rustc --explain E0063`.

@billylindeman

Copy link
Copy Markdown
Author

I could add this:

[[example]]
name = "bt_a2dp_sink_external_codec"
required-features = ["esp-idf-hal/pcnt-legacy"]

and update the example comment to include in the command

MCU=esp32 ESP_IDF_VERSION=master ESP_IDF_SDKCONFIG_DEFAULTS=.github/configs/sdkconfig.a2dp_external_codec cargo +esp espflash flash --target xtensa-esp32-espidf --features=esp-idf-hal/pcnt-legacy --example bt_a2dp_sink_external_codec --monitor

@ivmarkov

ivmarkov commented May 17, 2026

Copy link
Copy Markdown
Collaborator

The esp-idf-* crates do not yet claim compatibility with ESP-IDF 6.1. We build up to 6.0:

Do you absolutely need to use 6.1? If not (and I guess not) - you can downgrade your crate to 6.0 or even latest 5.x.

@ivmarkov

Copy link
Copy Markdown
Collaborator

... let alone ESP-IDF master, which is not very stable typically, and breaks APIs every other day.

@billylindeman

billylindeman commented May 18, 2026

Copy link
Copy Markdown
Author

@ivmarkov yes actually, this support was just merged in the last couple weeks

espressif/esp-idf#12114

espressif/esp-idf@84bdfef

@ivmarkov

ivmarkov commented May 19, 2026

Copy link
Copy Markdown
Collaborator

@ivmarkov yes actually, this support was just merged in the last couple weeks

espressif/esp-idf#12114

espressif/esp-idf@84bdfef

OK but then this PR cannot be merged until we do support 6.1 OR as part of this PR, the support for 6.1 is also brought in. Perhaps you can ask your LLM to look into it?

@ivmarkov

ivmarkov commented May 19, 2026

Copy link
Copy Markdown
Collaborator

(Sorry I over-assumed you use an LLM, perhaps you don't.)

In any case, the PCNT issue should not be super difficult to resolve. The reason why pcnt_chan_config_t__bindgen_ty_1 is not found during compilation is because likely the C pcnt_chan_config struct changed its layout in ESP-IDF 6.1, and therefore that exact bindgen-generated Rust sub-struct pcnt_chan_config_t__bindgen_ty_1 did disappear. This is typical for ESP-IDF structs which also contain C unions.

Recent ESP-IDF added a flexible-array-member field (`configs[]`) to
`wifi_event_dpp_config_received_t`. Bindgen now surfaces it as
`__IncompleteArrayField`, which (correctly) isn't `Clone` — a
variable-size struct can't be passed by value. The derive on the
newtype wrapper became impossible to satisfy.

`DppCfgRecvdRef` is only ever held as `&'a DppCfgRecvdRef` inside event
callbacks (see `WifiEvent::DppCfgRecvd`), so the derive was never
load-bearing. Drop it and document why.
@billylindeman
billylindeman force-pushed the billy/a2dp-external-codec-api branch from 1157cb8 to 6a04344 Compare June 3, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants