Portable audio

August 29, 2026 · View on GitHub

The audio surface is split by direction: boards expose audio_out and/or audio_in.

Playback: the audiosample contract

audio_out returns an audiodev.sample_out.AudioOut: play(sample, loop=False), stop(), pause(), resume(), playing. sample is anything satisfying CircuitPython's audiosample pull protocol — a synthio.Synthesizer, an audiomixer.Mixer, an audiocore.RawSample / WaveFile, or any effect chained on top of one (audiofilters, audiodelays, audiofreeverb, audiospeed), all provided by the audioif usermod on MicroPython or the separately installed pydevices-audioif CPython distribution (import synthio, import audiomixer, ...). CPython users install it from TestPyPI with:

python -m pip install --index-url https://test.pypi.org/simple/ pydevices-audioif

Neither pydevices distribution depends on it; importing audiodev and using raw PCM transports therefore remain available without synthesis support. This is the same object shape CircuitPython code already expects from audiobusio.I2SOut/audioio.AudioOut — a synth built for a CircuitPython tutorial runs unchanged against this port's AudioOut.

On CircuitPython boards (board_configs/cp/) the contract is satisfied natively: there is no audio_out role or AudioOut wrapper at all — CP apps construct audiobusio.I2SOut(...)/audioio.AudioOut(...) directly (see docs/board-peripherals.md: CP boards have no board_peripherals.py). Same protocol shape, zero adapter, because CircuitPython's own output devices already speak it.

On MicroPython, AudioOut is a pure-Python pump wrapping a push-based transport (below): it pulls PCM from the sample via audiocore.get_buffer()/reset_buffer() on a lookahead schedule and pushes it into the transport's write(). This is the only sound architecture here, not a stopgap: every real backend's bottom layer is a push+queued transport (SDL_QueueAudio, WASAPI, the wasm bridge, machine.I2S.write), and an audio-thread callback into Python is unsafe even in C once it would need to pull a synth's block graph, which allocates on the GC heap (documented in sdl2_audio.py). So playback flows only while something calls AudioOut.service() — the app tick, an asyncio pump, or attach(app) — same requirement every queued transport already has. No resampling: a sample's own sample_rate is not renegotiated against the transport; a mismatch plays at the wrong pitch rather than raising, exactly like real DAC hardware and exactly like CircuitPython's own AudioOut/I2SOut.

Low-level raw PCM streaming is still available as an escape hatch: every AudioOut.transport is (or behaves like) a PCMOutputwrite, drain, awrite, adrain, volume/mute, codec. audiodev.auto.audio_out() returns the bare transport for callers that want to push raw bytes without going through the sample-pull layer at all.

Capture and tones: unchanged

audio_in returns a PCMInput (readinto/areadinto); tone/buzzer roles return a ToneOutput (play(frequency, ...), frequency-based, not sample-based — unrelated to AudioOut.play(sample) above). Both report an AudioFormat plus a capabilities set; volume, gain, and mute use a normalized 0–100 scale and select hardware controls when a codec provides them, otherwise audiodev scales PCM in software.

Queued hosts also implement service(), queued_size(), is_active(), and clear() on the device (no-ops on MCU).

Codec-specific features remain reachable through device.codec. Shared half-duplex hardware uses AudioSession; opening the opposite direction while one direction owns the session raises OSError.

Host backends (transports under AudioOut, or usable raw)

audiodev.sdl2_audio is the reference playback and real-microphone backend for MicroPython. It uses queued SDL audio and provides the same sync and async contract as hardware devices.

audiodev.win_audio is the Windows WASAPI backend (uwin32), selected ahead of sdl2_audio on Windows when available.

audiodev.wasm_audio is the direct MicroPython WebAssembly backend. The compiled bridge owns Web Audio and permission state, while Python sees only neutral PCM buffers and queue operations. It supports mono/stereo 8-, 16-, and 32-bit signed or unsigned samples in either byte order at caller-selected rates. A host page must explicitly enable audio or microphone access first.

audiodev.i2s_audio adapts machine.I2S for MCU boards; boards construct it directly with board-specific pins/codec wiring, then wrap it in AudioOut.

Desktop board_peripherals may use audiodev.auto for host probing. Fixed host boards import a concrete backend. audiodev itself does not import displaydev.

CPython and Pyodide sample playback

audiodev.pygame_audio (CPython + pygame-ce, via SDL_QueueAudio on pygame's bundled SDL) and audiodev.web_audio (PyScript/Pyodide, via AudioContext/getUserMedia) still exist and still work as raw PCMOutput/PCMInput transports and can back AudioOut when pydevices-audioif is installed. Automatic selection uses MicroPython wasm, Pyodide Web Audio, Windows uwin32, usdl2, then pygame-ce, in that order. Constructing AudioOut without audiocore fails immediately with the install command above; direct raw write()/readinto() remains independent.

Emulated devices (CI / no hardware)

audiodev.emulated_audio implements WAV files, a waveform generator, in-memory loopback, and a discard sink. It is never auto-selected:

from audiodev import AudioFormat
from audiodev.emulated_audio import audio_in, audio_out

fmt = AudioFormat(24000, 1, 16)
out = audio_out(fmt, path="/sd/prompt.wav")
out.write(pcm_bytes)
out.close()

mic = audio_in(path="/sd/prompt.wav")

WavPCMOutput doubles as the deterministic golden-file target for testing the whole AudioOut chain — synthio/Mixer rendered through AudioOut over emulated_audio.audio_out(path=...), hash-compared, no audio hardware needed. See tests/test_audiodev.py.

ESP32-P4 status

The Waveshare ESP32-P4 configuration uses one half-duplex session for its I2S peripheral and ES8311. Playback exposes hardware DAC volume/mute and controls the speaker amplifier through AudioOut; capture exposes hardware ADC gain. Register, stream, session, and GPIO behavior is covered by host simulations.