5.1 KiB
BikeAudio — 3-board Bluetooth relay
Relays iPhone audio to two Bluetooth speakers (JBL Charge 5 + Cardo "Tangerine EDGE") at the same time.
Why three boards?
One ESP32 cannot do this alone:
- Sink + source can't coexist. To receive from the iPhone the ESP32 must be an A2DP sink; to play through a speaker it must be an A2DP source. The ESP32's classic-Bluetooth stack registers only one A2DP role at a time (the ESP32-A2DP library keeps a single global instance that each role overwrites). Running a sink and a source together orphans the sink — the iPhone can't even see it.
- A source reaches only one speaker. An A2DP source holds a single outgoing link, so one board can drive one speaker, not two.
So the job is split: one board receives, one board per speaker sends, and they pass audio between them over a short digital I2S wire bus.
iPhone ))BT)) ┌──────────────┐ I2S bus (BCK/WS/DATA + GND)
│ Board A │═══════════════╦═══════════════╗
│ A2DP SINK │ ║ ║
│ I2S MASTER │ ▼ ▼
└──────────────┘ ┌────────────┐ ┌────────────┐
│ Board B │ │ Board C │
│ A2DP SOURCE│ │ A2DP SOURCE│
│ I2S SLAVE │ │ I2S SLAVE │
└─────┬──────┘ └─────┬──────┘
))BT)) ))BT))
JBL Charge 5 Tangerine EDGE (Cardo)
Wiring
Board A is the I2S master (it generates the clocks). Boards B and C are slaves that listen to A's bus in parallel. Tie the three signals from A to the matching input pins on both B and C, and tie all grounds together.
| Signal | Board A (master, out) | Board B (slave, in) | Board C (slave, in) |
|---|---|---|---|
| Bit clock | GPIO5 (BCK) | GPIO19 | GPIO19 |
| Word select | GPIO25 (WS/LRCK) | GPIO18 | GPIO18 |
| Data | GPIO23 (DATA out) | GPIO22 (in) | GPIO22 (in) |
| Ground | GND | GND | GND |
- A·GPIO5 → B·GPIO19 and C·GPIO19
- A·GPIO25 → B·GPIO18 and C·GPIO18
- A·GPIO23 → B·GPIO22 and C·GPIO22
- A·GND → B·GND and C·GND (mandatory — shared clock reference)
Each board can be powered from its own USB/5V; only the grounds must be common.
Sync (touch pads on each source board)
JBL and Cardo have independent Bluetooth buffering, so one lags the other. Each source board has an adjustable delay (0–200 ms) you trim live, by ear, with two capacitive-touch pads — raise the delay on whichever speaker is early until they line up. The value is saved to flash (survives power-cycles).
| Touch pad | Pin | Action |
|---|---|---|
| + | GPIO4 | tap = +5 ms, hold = ramp up |
| − | GPIO27 | tap = −5 ms, hold = ramp down |
Attach a short wire or a bit of foil to GPIO4 and GPIO27 on each source board and touch the end. (There is intentionally no Wi-Fi/phone UI: Wi-Fi + Bluetooth + the audio buffer don't fit in RAM on the classic ESP32 — it starves the Bluetooth stack — so the control is local touch.)
Build & flash
pio run # builds all three
pio run -e sink # Board A
pio run -e source_jbl # Board B (target "JBL Charge 5")
pio run -e source_cardo # Board C (target "Tangerine EDGE")
Flash each board with the matching environment's artifacts
(.pio/build/<env>/{bootloader,partitions,firmware}.bin).
Power-on order: bring up Board A first so the I2S bus is clocking before B and C start reading it.
First-time pairing
- Put the JBL and Cardo in pairing mode.
- Power Board B and Board C — each connects to its speaker by name (auto-reconnects on later power-ups).
- Power Board A; on the iPhone, connect to "BikeAudio".
- Play audio — both speakers should output together.
Known limitations
- The two speakers are not sample-synchronized. JBL and Cardo each have their own Bluetooth buffering, so one lags the other. The per-board touch delay (above) lets you trim a fixed offset to line them up by ear; it is not continuous sample-lock, so slow drift over long sessions is possible. Fine for music/intercom; not suitable for tight stereo L/R separation.
- Audio is fixed at the SBC standard 44.1 kHz / 16-bit / stereo.
- If Board A reboots, the slave boards' audio pauses until A is clocking again.