Skip to content

feat(audio): add espp::Audio component; move the M5Stack Tab5 onto it - #881

Open
finger563 wants to merge 2 commits into
mainfrom
feat/audio-component
Open

finger563 wants to merge 2 commits into
mainfrom
feat/audio-component

Conversation

@finger563

@finger563 finger563 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

First step of moving the BSP audio code into its own component, the same way espp::Ethernet was split out.

New components/audio

espp::Audio drives one I2S port in standard mode with 16-bit PCM: playback, recording, or both (full duplex). It owns what every BSP used to copy:

  • the I2S channels and their DMA rings (12 × 240, checked against i2s_tx_feed::ring_outlasts_wait)
  • the playback stream buffer and the whole-block playback task (codec/i2s_tx_feed.hpp)
  • the recording task
  • volume, mute and microphone gain: in the codec, or volume and mute in software when there is no codec
  • sample-rate changes, which reconfigure both the I2S clock and the codecs
  • a full unwind when initialize() fails, plus deinitialize() and the destructor

Mono or stereo can be set per direction. Config::on_playback_block tells the BSP once per block whether that block carried audio.

Locking:

  • play() and clear() hold a stream mutex.
  • The codec setters hold a codec mutex.
  • The playback and recording tasks never take the lifecycle mutex, so teardown can join them while holding it.

Because of this, a recording callback may call play(), the setters, and start_recording() / stop_recording(). stop_recording() waits for a callback that is still running, unless it is called from inside that callback.

Codecs: chips plug in through a small virtual espp::AudioCodec interface (start / stop / set_sample_rate / set_volume / set_mute / set_gain). It is only called on init, teardown and setter calls, never per sample. This PR adds adapters on the existing codec drivers:

  • espp::Es8388Codec (playback)
  • espp::Es7210Codec (recording, with its high-pass filter and gain mapping). It now also reprograms its coefficient table when the sample rate changes; the Tab5 didn't do that before.

The component also has an example (a tone through a codec-less I2S amplifier, with the T-Deck's pins as defaults), docs (a new Audio section), Doxyfile entries and CI build/upload entries.

M5Stack Tab5

The BSP holds an espp::Audio and keeps only the board-specific parts: the pins, the codec I2C devices, the speaker amplifier, and the charger pause, driven by on_playback_block.

Its audio methods are now the canonical BSP audio API (documented in the component README):

  • New: deinitialize_audio(), is_audio_initialized(), audio() and clear_audio().
  • audio_sample_rate(uint32_t) now returns bool.
  • play_audio(const uint8_t *, size_t) now takes a size_t length.
  • initialize_audio(sample_rate, task_config) is [[deprecated]] in favor of initialize_audio(const AudioConfig &).
  • volume()/mute() before initialize_audio() now only store the value. Before, they called the ES8388 driver before its I2C functions were set.

The Tab5 example and the usb_host Tab5 example get a temporary main/idf_component.yml that overrides espp/audio to the in-repo path, as espp/ethernet did until it was published.

Memory

Internal RAM used by the Tab5's initialize_audio(), measured in the Tab5 example:

Version Bytes
main (in-BSP code) 64,312
first push of this PR 66,076
now 65,244 (+932)

Each std::mutex / std::recursive_mutex allocates its FreeRTOS semaphore (about 116 B of internal RAM) when it is constructed, and each espp::Logger holds one, so the second commit:

  • uses a single mutex in espp::Audio. Teardown releases it while it joins the tasks, and stop_recording() waits for a callback that is still running through an atomic flag.
  • has the codec adapters log through ESP_LOGx.
  • keeps only the I2S clock config.

Most of the remaining ~930 B is the Audio object itself (376 B, part of which used to be fields of the Tab5 object in static storage) and two semaphores: its own mutex's and its logger's.

tab5-emu with its current settings (recording and DMA2D on, about 17.5 KB of DMA-capable RAM free at the menu) has very little headroom; with the first push of this PR an SD save failed. With the #878 settings (recording and DMA2D off) it has about 43 KB and runs cleanly; see Testing.

Follow-ups (planned)

  • PR 2: the five ES8311 P4 BSPs, with an ES8311 adapter. This also fixes their 33 ms mono block size.
  • PR 3: esp-box, T-Deck, SC01 Plus and Cardputer. This needs TDM and PDM recording and separate ports, and deprecates initialize_sound / enable_sound / initialize_microphone.
  • A camera component after that.

Testing

All on a Tab5 with ESP-IDF 6.1.

  • Builds:
    • The audio example (esp32s3) builds with no warnings.
    • The Tab5 example builds, with no warnings outside managed components.
  • Tab5 example, with a temporary scripted self-test (not committed):
    • Playback: the click played.
    • Recording at 44.1 kHz: 44,266–44,320 Hz effective, with normal levels. The recording then played back.
    • deinitialize_audio() freed all 65 KB of internal RAM. Re-initializing used the same amount again (180,431 → 245,467 → 180,287 bytes free).
    • play_audio() after deinit returned 0.
    • After re-init, the click and a second recording worked.
  • After the memory trim, the same self-test passed again. It also checked that a callback calling stop_audio_recording() on itself ran exactly once, and that recording restarted afterwards.
  • tab5-emu soak after the trim (recording and DMA2D off): 7 sessions in 10 minutes, each with a save and a load. No errors and no resets. 81.9–82.9 KB internal RAM free at each launch, and 43.4 KB DMA-capable free at the menu.
  • tab5-emu soak before the trim, with the BSP and component from this branch, and recording and DMA2D off:
    • 7 sessions in 10 minutes (TPM, Jedi Knight, MGS, Chrono Trigger, Minish Cap, Sonic 2, Metroid Fusion), each with a save and a load.
    • No audio errors and no resets.
    • 81–82 KB internal and 42.7 KB DMA-capable RAM free at the menu.
    • With tab5-emu's current settings (recording and DMA2D on), the first save failed on SD DMA memory; see Memory.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY

New components/audio: espp::Audio drives one I2S port (standard mode,
16-bit PCM) for playback, recording or both, full duplex. It owns what
every BSP copied: the I2S channels and DMA rings, the playback stream
buffer and whole-block task (codec/i2s_tx_feed.hpp), the recording task,
volume / mute / microphone gain (in the codec, or in software without
one), sample rate changes, and a full unwind on failure plus
deinitialize() and the destructor.

Codecs plug in through the small espp::AudioCodec interface, with
adapters for the ES8388 (playback) and ES7210 (recording) on the codec
component's drivers. The ES7210 adapter also follows sample rate
changes (its coefficient table), which the Tab5 did not before.

The M5Stack Tab5 BSP now holds an espp::Audio and keeps the board parts
(pins, codec devices, speaker amplifier, charger pause via
Config::on_playback_block). Its audio API is the canonical BSP audio
API: adds deinitialize_audio(), is_audio_initialized(), audio(),
clear_audio(), and audio_sample_rate(uint32_t) returns bool;
initialize_audio(sample_rate, task_config) is deprecated in favor of
initialize_audio(AudioConfig). volume() / mute() before
initialize_audio() no longer call into an unwired codec driver.

Docs (new Audio section), Doxyfile, CI build / upload entries, and
example overrides for espp/audio until it is published.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY
Copilot AI balanced review requested due to automatic review settings November 4, 2026 15:52
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

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.

Copilot review overview

3 open issues
What changed in this PR

Adds a new espp::Audio component that centralizes I2S playback/recording and codec integration, and migrates the M5Stack Tab5 BSP to use it.

Changes:

  • Introduces components/audio with espp::Audio, espp::AudioCodec, and ES8388/ES7210 codec adapters plus an example project.
  • Refactors the Tab5 BSP audio implementation to delegate to espp::Audio, adding new BSP audio API methods and deprecating the old init overload.
  • Extends documentation/CI to build and publish the new component and include Audio docs.
File Description
doc/​en/​index.rst Adds an “Audio” section to the docs toctree.
doc/​en/​audio/​index.rst Introduces the Audio docs landing page and references.
doc/​en/​audio/​audio_example.md Includes the component’s example README into docs.
doc/​en/​audio/​audio.rst Documents espp::Audio, BSP audio API, and API reference includes.
doc/​Doxyfile Adds Audio headers and example to Doxygen inputs.
components/​usb_host/​tab5_example/​main/​idf_component.yml Adds example-only override for in-repo espp/audio.
components/​usb_host/​tab5_example/​CMakeLists.txt Adds components/audio to extra component dirs for the example.
components/​m5stack-tab5/​src/​audio.cpp Refactors Tab5 audio to wrap/use espp::Audio and codec adapters.
components/​m5stack-tab5/​include/​m5stack-tab5.hpp Updates Tab5 public audio API, adds new methods, deprecates old init signature.
components/​m5stack-tab5/​idf_component.yml Declares dependency on espp/audio and removes direct espp/codec dependency.
components/​m5stack-tab5/​example/​main/​idf_component.yml Adds example-only override for in-repo espp/audio.
components/​m5stack-tab5/​example/​CMakeLists.txt Adds components/audio to extra component dirs for the example.
components/​m5stack-tab5/​README.md Notes that Tab5 audio is built on espp::Audio and exposes it.
components/​m5stack-tab5/​CMakeLists.txt Switches build requirements from codec to audio.
components/​audio/​src/​es8388_codec.cpp Adds ES8388 playback adapter implementing AudioCodec.
components/​audio/​src/​es7210_codec.cpp Adds ES7210 recording adapter implementing AudioCodec, including HPF handling.
components/​audio/​src/​audio.cpp Adds the core espp::Audio implementation (I2S, tasks, buffering, teardown).
components/​audio/​include/​es8388_codec.hpp Declares the ES8388 codec adapter API.
components/​audio/​include/​es7210_codec.hpp Declares the ES7210 codec adapter API.
components/​audio/​include/​audio_codec.hpp Defines the codec abstraction interface used by espp::Audio.
components/​audio/​include/​audio.hpp Declares espp::Audio public API and configuration.
components/​audio/​idf_component.yml Defines the new component’s metadata and dependencies.
components/​audio/​example/​sdkconfig.defaults Sets example target and FreeRTOS tick rate defaults.
components/​audio/​example/​main/​audio_example.cpp Adds a playback-only example using software volume/mute.
components/​audio/​example/​main/​Kconfig.projbuild Adds example pin/power configuration options.
components/​audio/​example/​main/​CMakeLists.txt Registers the example’s main component.
components/​audio/​example/​README.md Documents building/running the audio example.
components/​audio/​example/​CMakeLists.txt Adds top-level CMake for the audio example project.
components/​audio/​README.md Adds component README describing features, codecs, and BSP API.
components/​audio/​CMakeLists.txt Registers the new audio component and dependencies.
.github/​workflows/​upload_components.yml Adds components/audio to the publish job list.
.github/​workflows/​build.yml Adds the audio example to CI builds.

Review effort: Lite

Comment thread components/audio/src/audio.cpp Outdated
Comment on lines +328 to +338
std::lock_guard<std::mutex> stream_lock(stream_mutex_);
if (!tx_stream_) {
return 0;
}
// Queue only whole frames that actually fit: xStreamBufferSend can accept
// fewer bytes than requested when the buffer is nearly full, and a partial
// frame would corrupt the framing of everything queued after it. With a 0
// timeout the send never blocks.
size_t sendable = std::min<size_t>(num_bytes, xStreamBufferSpacesAvailable(tx_stream_));
sendable -= sendable % tx_feed_.frame_bytes;
if (sendable == 0) {
Comment on lines +453 to +457
// wait for a callback in flight to return, so that none runs after this
// returns - unless this is that callback
if (recording_task_ && espp::Task::get_current_id() != recording_task_->get_id()) {
std::lock_guard<std::recursive_mutex> callback_lock(recording_callback_mutex_);
}
Comment on lines +75 to +83
static constexpr uint8_t ES7210_ADDR = 0x40; // the driver's (AD1/AD0 = 00)
bool hpf_ok = true;
for (auto [reg, value] : {std::pair<uint8_t, uint8_t>{0x22, 0x0a}, // ADC1/2 HPF1
{0x23, 0x2a}, // ADC1/2 HPF2
{0x20, 0x0a}, // ADC3/4 HPF2
{0x21, 0x2a}}) { // ADC3/4 HPF1
uint8_t data[] = {reg, value};
hpf_ok &= config_.write(ES7210_ADDR, data, sizeof(data));
}
On this platform every std::mutex / std::recursive_mutex allocates its
FreeRTOS semaphore (about 116 B of internal RAM) and every espp::Logger
carries one too, so the component's four mutexes and the adapters'
loggers were most of its overhead over the old in-BSP code.

- espp::Audio uses a single mutex. Teardown releases it while it joins
  the tasks (tearing_down_ keeps initialize() / deinitialize() out
  meanwhile), so a callback that calls a method cannot deadlock against
  it, and stop_recording() waits for a callback in flight through an
  atomic flag instead of a mutex.
- The ES8388 / ES7210 adapters log through ESP_LOGx instead of an
  espp::Logger, and the ES7210 keeps only what start() needs.
- Only the I2S clock configuration is kept (for sample_rate()), not two
  full standard-mode configurations.

M5Stack Tab5 initialize_audio(): 66,076 -> 65,244 B of internal RAM
(main's in-BSP code: 64,312 B).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY

This branch has not been deployed

No deployments
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