feat(bsp): microphone record/playback and tabbed example UIs for audio-capable boards#674
Conversation
…o-capable boards Give the BSP components that have audio input a consistent way to exercise recording, playback, and volume control, and reorganize their examples into a multi-page UI so the widgets are no longer crammed onto one screen. Example UI (esp-box, t-deck, m5stack-tab5, esp32-p4-function-ev-board, smartpanlee-sc01-plus): - Reorganize each example into a tabbed (lv_tabview) UI so the IMU drawing, audio controls, and other widgets each get their own page. Drawing is confined to its own page, so swiping between tabs no longer smears points onto the other pages. - Wrapping tab labels (still readable when the display is rotated) and speaker icons on the volume buttons. Microphone support: - esp-box, t-deck, m5stack-tab5: on-screen record/playback GUI and a microphone-volume API, reading the ES7210 ADC. - esp32-p4-function-ev-board: record/playback GUI reading the ES8311 ADC. - m5stack-cardputer: microphone-volume API and function-key volume controls. - play_audio is now streaming-friendly (append what fits, report how many bytes were queued) so callers can stream buffers larger than the speaker stream buffer, and playback keeps that buffer topped up to avoid underrun static. On-screen volume labels stay in sync with the live BSP state. m5stack-tab5 (full-duplex ES8388 + ES7210) audio: - Fix the RX channel to share the TX standard-mode clock, enable the ES7210 high-pass filter, and drain RX from a dedicated microphone task (blocking read) rather than the shared playback task. On this single full-duplex controller an irregularly-drained RX ring perturbs the TX and both degrades the recording and pops the playback; the dedicated task mirrors the well-behaved esp-box path and yields clean capture. - The example applies DC removal and an RMS-normalized software makeup gain to each recording, so it plays back at an audible level from a low analog mic gain (the ES7210 front-end whines when the analog gain is driven hard). T-Deck ES7210 microphone (needed the most work): - Read the ES7210 as its fixed 4-slot TDM frame and keep the two populated microphones (MIC1 / MIC3); a narrower read misframes the codec. - Enable the ES7210 digital high-pass filter so the mic DC offset stops railing the ADC, and let the driver derive bclk_div so MCLK matches the codec coefficient table (a forced divider was slipping frames). - Run the example at 16 kHz so playback pitch is correct, and make the trackball center button optional (GPIO0 conflicts with the mic). - The mic is genuinely low-sensitivity and picks up random electrical impulse noise, so the example post-processes each recording: de-glitch (mark and interpolate), DC removal, and an RMS-normalized software makeup gain. This is the practical limit of the board's microphone hardware, documented in the example README. Example configuration (kept out of the BSP defaults): - Raise the LVGL task stacks, and on esp-box / t-deck the interrupt/touch task stack, through each example's sdkconfig.defaults so existing users of the components are unaffected; enable the end-of-stack watchpoint on those two boards and PSRAM where the recording buffers need it. All documented in the example READMEs. - Lower the m5stack-tab5 I/O-expander init log verbosity (INFO -> WARN). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR adds consistent microphone record/playback and volume control capabilities across audio-capable BSPs, and reorganizes several board examples into tabbed LVGL UIs to avoid overcrowding and drawing artifacts.
Changes:
- Add microphone initialization/capture APIs (ES7210/ES8311) and example record/playback flows.
- Make
play_audio()streaming-friendly by returning the number of bytes actually queued. - Rework multiple example GUIs into
lv_tabviewpages and adjust examplesdkconfig.defaultsfor stack/PSRAM needs.
Reviewed changes
Copilot reviewed 48 out of 48 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| components/t-deck/src/t-deck.cpp | Make trackball center button optional to avoid GPIO0 mic conflicts |
| components/t-deck/src/audio.cpp | Add ES7210 mic capture path; make play_audio return queued bytes |
| components/t-deck/include/t-deck.hpp | Expose mic API + play_audio() return value and trackball option |
| components/t-deck/example/sdkconfig.defaults | Enable PSRAM and increase task stacks for the example |
| components/t-deck/example/main/t_deck_example.cpp | Add record/playback UI wiring + 16 kHz fixed-rate pipeline |
| components/t-deck/example/main/gui.hpp | Convert example UI to tabview + add audio widgets/callbacks |
| components/t-deck/example/main/gui.cpp | Implement tabbed Draw/Audio UI and live volume/status labels |
| components/t-deck/example/README.md | Document stack/PSRAM config and T-Deck mic quality limitations |
| components/t-deck/README.md | Document dual-mic recording support via ES7210 |
| components/t-deck/CMakeLists.txt | Add codec dependency for ES7210 usage |
| components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp | Make play_audio() return queued byte count |
| components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp | Document new play_audio() return semantics |
| components/smartpanlee-sc01-plus/example/sdkconfig.defaults | Increase interrupt stack for touch error logging |
| components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp | Route drawing to Draw tab; add Audio-tab play callback |
| components/smartpanlee-sc01-plus/example/main/gui.hpp | Tabview UI + audio controls callback plumbing |
| components/smartpanlee-sc01-plus/example/main/gui.cpp | Implement tabview + audio controls and label updates |
| components/smartpanlee-sc01-plus/example/README.md | Document example audio controls + stack override rationale |
| components/m5stack-tab5/src/m5stack-tab5.cpp | Reduce IO expander init log verbosity |
| components/m5stack-tab5/src/audio.cpp | Fix full-duplex RX config; add mic HPF + mic task + streaming play_audio |
| components/m5stack-tab5/include/m5stack-tab5.hpp | Update audio API return values + add mic volume API |
| components/m5stack-tab5/example/sdkconfig.defaults | Increase interrupt stack size for touch error logging |
| components/m5stack-tab5/example/main/m5stack_tab5_example.cpp | Add record/playback + post-processing + tabbed UI usage |
| components/m5stack-tab5/example/main/gui.hpp | Convert to tabview and add Audio tab controls/callbacks |
| components/m5stack-tab5/example/main/gui.cpp | Implement Draw/Status/Audio tabs and volume/status widgets |
| components/m5stack-tab5/example/README.md | Document recording/playback UI and stack override rationale |
| components/m5stack-cardputer/src/microphone.cpp | Add mic volume control (ES8311 hw or PDM software scaling) |
| components/m5stack-cardputer/src/m5stack-cardputer.cpp | Apply stored mic volume during ES8311 mic init |
| components/m5stack-cardputer/include/m5stack-cardputer.hpp | Expose microphone_volume() API and docs |
| components/m5stack-cardputer/example/main/m5stack_cardputer_example.cpp | Add function-key speaker/mic volume shortcuts |
| components/m5stack-cardputer/example/main/gui.hpp | Update help text with new volume shortcuts |
| components/m5stack-cardputer/example/README.md | Document new keybindings for speaker/mic volume |
| components/m5stack-cardputer/README.md | Document mic volume behavior across variants |
| components/esp32-p4-function-ev-board/src/audio.cpp | Add ES8311 ADC mic capture task + streaming-friendly playback |
| components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp | Expose mic API + play_audio() return values |
| components/esp32-p4-function-ev-board/example/sdkconfig.defaults | Increase interrupt/touch stacks for touch error logging |
| components/esp32-p4-function-ev-board/example/main/gui.hpp | Convert example UI to tabview + add Audio controls |
| components/esp32-p4-function-ev-board/example/main/gui.cpp | Implement Status/Audio tabs and record/play/volume widgets |
| components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp | Add record/playback buffer + tab-limited drawing |
| components/esp32-p4-function-ev-board/example/README.md | Document recording UI and stack overrides |
| components/esp32-p4-function-ev-board/README.md | Document full-duplex ES8311 playback + recording API |
| components/esp-box/src/audio.cpp | Add ES7210 mic support + clamp volume + return queued bytes |
| components/esp-box/include/esp-box.hpp | Expose mic API + streaming-friendly play_audio() signature |
| components/esp-box/example/sdkconfig.defaults | Enable PSRAM + stack watchpoint + increase interrupt stack |
| components/esp-box/example/main/gui.hpp | Convert example UI to tabview + add Audio controls/callbacks |
| components/esp-box/example/main/gui.cpp | Implement Draw/IMU/Audio tabs and live volume/status labels |
| components/esp-box/example/main/esp_box_example.cpp | Add mic record/playback buffering + tab-limited drawing |
| components/esp-box/example/README.md | Document audio recording/playback UI behavior and stack overrides |
| components/esp-box/README.md | Document full-duplex audio and mic recording pipeline |
Comments suppressed due to low confidence (1)
components/t-deck/src/audio.cpp:1
xStreamBufferSendFromISR()is intended for ISR context; calling it from normal task context is undefined/not portable across FreeRTOS ports. Consider switching toxStreamBufferSend(..., 0)for non-blocking task calls, or implement a small helper that usesxPortInIsrContext()to choose betweenxStreamBufferSend()andxStreamBufferSendFromISR().
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 49 out of 49 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
components/esp-box/src/audio.cpp:237
- play_audio() can enqueue a byte count that is not aligned to 16-bit stereo frames (4 bytes). If a caller streams an odd/non-frame-aligned size, the stream buffer can end up with partial samples, which will shift audio framing and may leave 1–3 bytes that the audio task never drains cleanly.
// don't block here: append what fits into the stream buffer and report how
// much was actually queued so callers can stream data larger than the buffer.
// This runs in task context, so use the non-ISR send with a 0 timeout (never
// blocks) rather than the FromISR variant.
return xStreamBufferSend(audio_tx_stream, data, num_bytes, 0);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 50 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (3)
components/t-deck/include/t-deck.hpp:1
- The PR description (and checkbox) indicates a “non-breaking change”, but changing
play_audio(...)fromvoidtosize_tis an API-breaking change for downstream callers (compile-time break). Consider keeping the oldvoidoverloads as wrappers (possibly marked deprecated) that forward to the newsize_tversions, or introduce a new method name (e.g.,enqueue_audio(...)) while keeping the existing signature stable.
components/t-deck/src/audio.cpp:1 - If
es7210_adc_config_i2s(...)fails, the code logs but continues as if the microphone is configured. This can leave the capture pipeline in a misconfigured state while still enabling the RX channel and starting the task. Consider treating this as a hard failure: returnfalseand perform the same cleanup path used for other init failures (disable/delete I2S channel, reset handles, etc.).
components/t-deck/src/audio.cpp:1 - A single
std::error_code hpf_ecis reused across four register writes; depending on howwrite_registerupdateshpf_econ success, this can (a) report only the last failing write, or (b) keep a stale error value and warn even if a later write succeeds. Consider resetting/checking the error code per write (or logging which register failed) so HPF configuration failures are diagnosable and the warning reflects the actual failure.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 50 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (6)
components/t-deck/example/main/t_deck_example.cpp:1
- The microphone callback writes into
recording_bufferconcurrently with the main loop, and the main loop begins post-processing the same buffer immediately when it observesrecordingtransition to false. Because an in-flight callback invocation may have already passed itsif (!recording) return;check, this can create a data race (concurrentmemcpyvs post-processing reads/writes), which is undefined behavior in C++. A concrete fix is to add a synchronization point when stopping recording (e.g., stop/suspend the microphone delivery before processing, or use a mutex/sequence counter so the main loop waits until no callback is active before touchingrecording_buffer).
components/m5stack-tab5/src/audio.cpp:1 - On either task start failure path, the function returns
falsebut leaves previously-initialized audio resources (I2S channels enabled, codec started/configured, stream buffers allocated) in a partially-initialized state. This can cause resource leaks and unpredictable behavior if the caller retries initialization or continues using the object. Consider adding explicit teardown/rollback here (disable/del I2S channels, stop codec ADC/DAC as appropriate, release buffers) so failure is cleanly recoverable.
components/t-deck/src/audio.cpp:1 initialize_microphone()returnsbooland already uses error-return patterns, but it also usesESP_ERROR_CHECK(...)for multiple calls; those will hard-abort the program on failure instead of returningfalsewith cleanup/logging. For consistency and better robustness (especially on boards where peripherals may be absent/miswired), prefer checking the return codes and performing the same cleanup paths you already have (disable/del channel, null handles, etc.) rather than aborting.
components/t-deck/src/audio.cpp:1initialize_microphone()returnsbooland already uses error-return patterns, but it also usesESP_ERROR_CHECK(...)for multiple calls; those will hard-abort the program on failure instead of returningfalsewith cleanup/logging. For consistency and better robustness (especially on boards where peripherals may be absent/miswired), prefer checking the return codes and performing the same cleanup paths you already have (disable/del channel, null handles, etc.) rather than aborting.
components/t-deck/src/audio.cpp:1initialize_microphone()returnsbooland already uses error-return patterns, but it also usesESP_ERROR_CHECK(...)for multiple calls; those will hard-abort the program on failure instead of returningfalsewith cleanup/logging. For consistency and better robustness (especially on boards where peripherals may be absent/miswired), prefer checking the return codes and performing the same cleanup paths you already have (disable/del channel, null handles, etc.) rather than aborting.
components/smartpanlee-sc01-plus/example/main/gui.cpp:1last_volume/last_mutedarestaticfunction locals, so they’re shared across allGuiinstances and survive across deinit/reinit cycles; they also differ from the member-variable approach used in the other GUIs in this PR. Prefer making these cached values instance members (likelast_speaker_volume_/last_mic_volume_elsewhere) so state is correctly scoped and reset with theGuiobject lifecycle.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 50 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (3)
components/t-deck/src/audio.cpp:1
play_audio()now unconditionally usesxStreamBufferSend()(task API) whereas the previous implementation usedxStreamBufferSendFromISR(). If any existing caller invokesplay_audio()from ISR context (or from an IDF callback that can run in ISR context), this can trigger FreeRTOS asserts or undefined behavior at runtime. Consider making this robust by branching on ISR context (e.g.,xPortInIsrContext()): usexStreamBufferSendFromISR()when in ISR andxStreamBufferSend()otherwise, while still enforcing frame alignment, or add a dedicated ISR-safeplay_audio_from_isr()API if ISR use is intended.
components/t-deck/src/audio.cpp:1audio_rx_bufferis astd::vector<uint8_t>, soaudio_rx_buffer.data()is only byte-aligned by the C++ standard; reinterpreting and dereferencing it asint16_t*is undefined behavior due to potential misalignment (even if it often “works” on some targets). A concrete fix is to store the RX buffer asstd::vector<int16_t>(or another explicitly aligned allocation), perform compaction in that typed buffer, and passreinterpret_cast<const uint8_t*>(samples)to the callback with a byte length.
components/smartpanlee-sc01-plus/example/main/gui.cpp:1last_volume/last_mutedarestaticfunction locals, which makes the cache shared across allGuiinstances and persist across deinit/reinit. This can cause the label to stop updating in scenarios like recreating the GUI (or having multiple GUIs in tests). Store these asGuimember fields (similar to thelast_speaker_volume_/last_mic_volume_pattern used in the other tabbed UIs) so caching is instance-scoped.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 50 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
components/t-deck/src/audio.cpp:1
- The default branch silently maps unsupported
ratevalues toAUDIO_HAL_16K_SAMPLESwithout also forcing the I2S clock (sample_rate_hz) to 16 kHz. That creates a codec coefficient-table mismatch (the exact issue this PR is trying to avoid) if a caller passes an unsupported rate. Prefer making unsupported rates a hard error (return false / log) or normalize the actual configured sample rate (use the fallback rate consistently for both I2S and codec).
components/t-deck/src/audio.cpp:1 - This change makes
play_audio()explicitly task-context only (usesxStreamBufferSend). Previously, the implementation usedxStreamBufferSendFromISR, which likely made some downstream code assume ISR-safety. If ISR callers exist, they will now fail at runtime. To keep the API robust, consider supporting both contexts (e.g., detect ISR context and route toxStreamBufferSendFromISRwith frame-aligned sizing), or provide a dedicatedplay_audio_from_isr(...)entry point while preserving the current task-only contract.
Description
Give the BSP components that have audio input a consistent way to exercise recording, playback, and volume control, and reorganize their examples into a multi-page UI so the widgets are no longer crammed onto one screen.
Example UI (esp-box, t-deck, m5stack-tab5, esp32-p4-function-ev-board, smartpanlee-sc01-plus):
Microphone support:
m5stack-tab5 (full-duplex ES8388 + ES7210) audio:
T-Deck ES7210 microphone (needed the most work):
Example configuration (kept out of the BSP defaults):
Motivation and Context
Allows users to exercise the microphone and speaker on all BSP boards that have them, and gives a consistent UI and API for doing so.
The T-Deck microphone is low-sensitivity and noisy, so the example post-processes the recording to make it audible.
How has this been tested?
Build and run the examples on the esp-box, t-deck, m5stack-tab5, esp32-p4-function-ev-board, and m5stack-cardputer. The examples exercise the microphone and speaker, and the volume controls are verified to work.
Screenshots (if appropriate, e.g. schematic, board, console logs, lab pictures):
Types of changes
Checklist:
Software
.github/workflows/build.ymlfile to add my new test to the automated cloud build github action.