These tests exercise the DAC-based audioio.AudioOut implementation added for STM32F405xx and STM32F407xx.
| Test | Automated | Requires audio/scope |
|---|---|---|
| 1 — WAV File Playback | Yes | Yes (audio check) |
| 2 — Pause / Resume | Yes | Yes (audio check) |
| 3 — Looping Sine Wave | Yes | Yes (audio check) |
| 4 — deinit and Re-init | Yes | No |
| 5 — Stereo Playback | Yes | Yes (audio check) |
run_serial_tests.py automates Tests 1–5: it copies the necessary files to the
board and runs each script over the serial REPL, comparing the printed output to
the expected patterns. You still need to listen to the audio (and optionally
use an oscilloscope) for the audio-quality checks.
# Install the one dependency (if not already present)
pip install mpremote
# Run all automated tests (board must be connected and CIRCUITPY mounted)
python3 tests/circuitpython-manual/audioio/run_serial_tests.py
# Skip file copy if files are already on the board
python3 tests/circuitpython-manual/audioio/run_serial_tests.py --no-copy
# Override the serial port or CIRCUITPY path (auto-detected on macOS/Linux/Windows)
python3 tests/circuitpython-manual/audioio/run_serial_tests.py \
--port /dev/cu.usbmodem1234 \
--circuitpy /Volumes/CIRCUITPY # macOS (auto-detected)
python3 tests/circuitpython-manual/audioio/run_serial_tests.py \
--port /dev/ttyACM0 \
--circuitpy /media/user/CIRCUITPY # Linux (auto-detected)
python3 tests/circuitpython-manual/audioio/run_serial_tests.py \
--port COM5 \
--circuitpy D:\ # Windows (auto-detected)
# Run only specific tests
python3 tests/circuitpython-manual/audioio/run_serial_tests.py --tests 3,4,5The script exits 0 if all selected tests pass, 1 otherwise — suitable for CI.
- An STM32F405 or STM32F407 board running the freshly-built firmware (e.g. Feather STM32F405 Express).
- A passive speaker or audio amplifier connected to pin A0 (PA04, DAC channel 1) and GND.
- A simple test: 100 Ω resistor in series with a small speaker between A0 and GND.
- For better audio: connect A0 to a small amp module (e.g. PAM8403) then to a speaker.
- Optional: an oscilloscope or logic analyser probe on D4 (used as a trigger output by the test scripts).
- A USB cable for REPL/filesystem access (CircuitPython storage must not be read-only).
Enable the feature by building for an F405 or F407 target. CIRCUITPY_AUDIOIO
is set to 1 automatically for those variants.
make -C ports/stm BOARD=feather_stm32f405_express -j
Verify the option is enabled in the generated build config:
grep CIRCUITPY_AUDIOIO ports/stm/build-feather_stm32f405_express/mpconfigport.mk
# Expected output: CIRCUITPY_AUDIOIO = 1
Flash the resulting .bin to the board using your preferred method (e.g. dfu-util):
dfu-util -a 0 --dfuse-address 0x08000000:force:mass-erase -D ports/stm/build-feather_stm32f405_express/firmware.bin
If using
run_serial_tests.pythis step is done automatically — skip ahead.
Copy the WAV test samples onto the board's CIRCUITPY drive root:
cp \
tests/circuitpython-manual/audiocore/jeplayer-splash-8000-8bit-mono-unsigned.wav \
tests/circuitpython-manual/audiocore/jeplayer-splash-8000-16bit-mono-signed.wav \
tests/circuitpython-manual/audiocore/jeplayer-splash-44100-16bit-mono-signed.wav \
tests/circuitpython-manual/audiocore/jeplayer-splash-8000-16bit-stereo-signed.wav \
tests/circuitpython-manual/audiocore/jeplayer-splash-44100-16bit-stereo-signed.wav \
/Volumes/CIRCUITPY/
These five files cover every exercised code path:
8000-8bit-mono-unsigned— 8-bit unsigned decode path, audibly lo-fi8000-16bit-mono-signed— 16-bit signed decode path at lowest sample rate44100-16bit-mono-signed— 16-bit at highest sample rate (DMA reconfiguration, audible quality difference)8000-16bit-stereo-signed— stereo decode path, left→A0, right→A144100-16bit-stereo-signed— stereo at 44.1 kHz
The 16 kHz files in the audiocore test set are skipped because they exercise
no code paths beyond 8 kHz and 44.1 kHz. 24-bit WAVs are not supported by
audiocore.WaveFile and will raise OSError if loaded — they are omitted on
purpose.
Copy the test scripts to the board as well (or paste them into the REPL):
cp tests/circuitpython-manual/audioio/wavefile_playback.py /Volumes/CIRCUITPY/
cp tests/circuitpython-manual/audioio/wavefile_pause_resume.py /Volumes/CIRCUITPY/
cp tests/circuitpython-manual/audioio/single_buffer_loop.py /Volumes/CIRCUITPY/
cp tests/circuitpython-manual/audioio/stereo_playback.py /Volumes/CIRCUITPY/
Verifies that AudioOut(board.A0) can play WAV files at 8 kHz and 44.1 kHz
with 8-bit unsigned and 16-bit signed encodings. Stereo WAVs are played here
through the mono AudioOut (only the left channel is mixed to A0); the
stereo path is exercised separately in Test 5.
Note: 24-bit WAV files are not supported by audiocore.WaveFile and will
print an OSError if any are present on the filesystem. That is expected.
Run from the REPL:
import os
os.chdir("/")
exec(open("wavefile_playback.py").read())Expected output (order may vary by filename sort):
playing jeplayer-splash-44100-16bit-mono-signed.wav
playing jeplayer-splash-8000-16bit-mono-signed.wav
playing jeplayer-splash-8000-8bit-mono-unsigned.wav
done
What to listen for:
- Each supported WAV plays the "jeplayer splash" jingle to completion before the next starts.
- No loud pops at the start or end of each file (the DAC ramp-in / ramp-out should suppress them).
- Audio pitch should match the sample rate: the 44100 Hz file sounds the most natural; the 8000 Hz file sounds lower fidelity.
Verifies AudioOut.pause() / AudioOut.resume() by toggling every 100 ms during playback. The audio will sound choppy — that is intentional.
Run from the REPL:
exec(open("wavefile_pause_resume.py").read())Expected output (repeating for each WAV):
playing with pause/resume: jeplayer-splash-44100-16bit-mono-signed.wav
paused
resumed
...
playing with pause/resume: jeplayer-splash-8000-16bit-mono-signed.wav
paused
resumed
...
playing with pause/resume: jeplayer-splash-8000-8bit-mono-unsigned.wav
paused
resumed
...
done
What to verify:
- The REPL prints alternating "paused" / "resumed" lines.
- Audio cuts in and out in sync with the prints.
- Playback eventually completes (the
while dac.playingloop exits normally). - No hard fault or hang.
Verifies RawSample with loop=True and tests all four sample formats:
unsigned 8-bit, signed 8-bit, unsigned 16-bit, signed 16-bit.
Each sample generates one cycle of a 440 Hz sine wave and loops for 1 second.
Run from the REPL:
exec(open("single_buffer_loop.py").read())Expected output:
unsigned 8 bit
signed 8 bit
unsigned 16 bit
signed 16 bit
done
What to listen for:
- A 440 Hz tone (concert A) for approximately 1 second for each format.
- All four formats use the same 8 kHz sample rate and should sound essentially identical in pitch and volume — the test is comparing format-conversion paths, not playback rates.
- No pops or glitches during the loop.
- Clean silence between tones (quiescent DAC value holds between
stop()calls).
Verifies that AudioOut can be deconstructed and reconstructed without rebooting, and that pin A0 is properly released.
import audioio, analogio, board
# Construct and immediately deinit AudioOut
dac = audioio.AudioOut(board.A0)
dac.deinit()
# PA04 should now be free for AnalogOut
aout = analogio.AnalogOut(board.A0)
aout.value = 32768 # mid-scale
aout.deinit()
# Re-create AudioOut on the same pin
dac2 = audioio.AudioOut(board.A0)
dac2.deinit()
print("pass")Expected output: pass with no exceptions.
Verifies that AudioOut(board.A0, right_channel=board.A1) drives both DAC
channels independently: left on A0 (PA04, DAC_CH1), right on
A1 (PA05, DAC_CH2), both clocked by TIM6.
The script runs four phases in order:
- Left-only 440 Hz tone (~1 s) — only A0 should produce audio.
- Right-only 440 Hz tone (~1 s) — only A1 should produce audio.
- Both-channel 440 Hz tone (~1 s) — equal amplitude on both pins.
- Pan sweep L → R (~3 s) — continuous equal-power (cos/sin) crossfade from A0 to A1 in a single non-looped buffer.
- Then plays each stereo WAV (
44100and8000Hz) in full.
Hardware required: connect a stereo headphone/amp to A0 (left) and A1 (right) with common ground, or scope-probe each pin separately.
Run from the REPL:
import os
os.chdir("/")
exec(open("stereo_playback.py").read())Expected output:
channel test: left only
channel test: right only
channel test: both channels
pan sweep: left to right
playing stereo: jeplayer-splash-44100-16bit-stereo-signed.wav
playing stereo: jeplayer-splash-8000-16bit-stereo-signed.wav
done
What to listen / look for:
- "left only" → tone in left ear, silence in right.
- "right only" → tone in right ear, silence in left.
- "both channels" → centered tone in both ears.
- "pan sweep" → tone smoothly travels left → right over ~3 s.
- Stereo WAVs play with proper L/R separation; no cross-contamination.
- On a scope: probing A0 and A1 simultaneously during phases 1 and 2 should show one channel idle (mid-scale DC) while the other carries the sine.
Each test script drives board.D4 (pin D4) low at the start of each playback and high when it ends. This provides a clean trigger edge for a scope.
- Test 1: Probe A0 — should show a sampled waveform at the correct sample rate. Probe D4 for a gate signal that spans the file duration.
- Test 3: Probe A0 — should show a 440 Hz staircase-sine at the DAC output (12-bit steps visible at 44.1 kHz; fewer at 8 kHz). A simple RC low-pass filter (1 kΩ + 100 nF) on the A0 output will smooth the staircase significantly.
- Left channel must be A0 (PA04). Any other pin raises
ValueError: AudioOut requires pin A0 (PA04). - Right channel must be A1 (PA05) when used. Any other pin raises
ValueError: AudioOut right channel requires pin A1 (PA05). - 24-bit WAV files are not supported by
audiocore.WaveFileand will raiseOSErrorwhen opened. - Only one
AudioOutinstance can be active at a time.