Skip to content

Soundboard Speaker

Works with

Any CircuitPython board with I2S audio — Feather RP2040, QT Py RP2040, Feather M4

Level: Builder

This project is exactly what it sounds like: a box with buttons that each trigger a different sound effect through a real speaker. Motorcycle engine, air horn, laser blast, crowd cheering — whatever WAV files you drop onto the board. It is a good introduction to I2S digital audio and working with files from the CIRCUITPY filesystem.

What you'll build

A panel with arcade-style push buttons connected to a microcontroller and a small speaker. Pressing any button plays the corresponding audio file immediately, interrupting whatever was playing before. The board's CIRCUITPY drive holds the WAV files directly — no SD card needed for short clips.

What you'll need

  • Feather RP2040, Feather M4, or any CircuitPython board with I2S support
  • I2S audio amplifier breakout — the Adafruit MAX98357A is a good choice (~$4)
  • Small 4-ohm or 8-ohm speaker (1–3 W)
  • Two or more momentary push buttons (arcade buttons feel great for this)
  • Short WAV files in 16-bit, 22050 Hz mono format (convert with Audacity or ffmpeg)
  • Breadboard and jumper wires

Keep your WAV files small — the Feather RP2040 has 8 MB of flash, and CIRCUITPY only gets a portion of that. Mono 22 kHz audio at 16-bit is about 44 KB per second. Ten seconds of audio per sound effect is plenty.

Wiring

The I2S amplifier needs three signal lines (BCLK, LRCLK, DATA) plus power. Each button connects between a digital input pin and GND.

graph LR
    subgraph Board
        BCLK["BCLK (I2S bit clock)"]
        LRCLK["LRCLK (I2S word select)"]
        DATA["DATA (I2S data out)"]
        B1["Digital D9"]
        B2["Digital D10"]
        B3["Digital D11"]
        GND["GND"]
        V3["3.3V"]
    end

    subgraph MAX98357A
        AMP_BCLK["BCLK"]
        AMP_LR["LRC"]
        AMP_DIN["DIN"]
        AMP_VIN["Vin"]
        AMP_GND["GND"]
        AMP_OUT["Speaker +/-"]
    end

    BCLK --> AMP_BCLK
    LRCLK --> AMP_LR
    DATA --> AMP_DIN
    V3 --> AMP_VIN
    GND --> AMP_GND
    AMP_OUT --> SPK["Speaker"]

    B1 --> SW1["Button 1"] --> GND
    B2 --> SW2["Button 2"] --> GND
    B3 --> SW3["Button 3"] --> GND

Check the pinout diagram for your specific board to find the I2S BCLK, LRCLK, and DATA pins — they are labeled differently on different boards (sometimes BCLK/WSEL/DOUT, sometimes I2S_BCLK/I2S_WS/I2S_TX).

The code

import board
import time
import digitalio
import audiobusio
import audiocore

# I2S amplifier setup — adjust pins for your board
i2s = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DATA)

# Sound files on the CIRCUITPY drive
SOUNDS = [
    "sounds/engine.wav",
    "sounds/horn.wav",
    "sounds/laser.wav",
]

# Button setup
BUTTON_PINS = [board.D9, board.D10, board.D11]
buttons = []
for pin in BUTTON_PINS:
    b = digitalio.DigitalInOut(pin)
    b.direction = digitalio.Direction.INPUT
    b.pull = digitalio.Pull.UP
    buttons.append(b)

prev_states = [True] * len(buttons)

def play_sound(filename):
    if i2s.playing:
        i2s.stop()
    wave = audiocore.WaveFile(open(filename, "rb"))
    i2s.play(wave)

while True:
    for i, button in enumerate(buttons):
        current = button.value
        if prev_states[i] and not current:
            # Button just pressed
            if i < len(SOUNDS):
                play_sound(SOUNDS[i])
        prev_states[i] = current
    time.sleep(0.01)

Create a sounds/ folder on your CIRCUITPY drive and copy your WAV files there. Use Audacity or ffmpeg -i input.mp3 -ar 22050 -ac 1 -acodec pcm_s16le output.wav to convert files to the right format.

How it works

I2S protocol for digital audio. I2S (Inter-IC Sound) is a serial bus protocol designed specifically for transmitting digital audio between chips. Three wires carry all the information: BCLK (bit clock, which ticks once per bit), LRCLK (left/right clock, which marks whether the current sample is for the left or right channel), and DATA (the actual audio samples, one bit per BCLK tick). The MAX98357A receives these digital samples, converts them to an analog signal internally, and drives the speaker directly. Because the conversion happens on the amplifier chip rather than on the microcontroller, the audio quality is much better than PWM output.

Loading WAV files from the CIRCUITPY drive. CircuitPython mounts the board's flash storage as a USB drive called CIRCUITPY. Files you copy there are accessible from Python using normal file I/O — open("sounds/engine.wav", "rb") works exactly as you would expect. The audiocore.WaveFile class wraps the open file handle and handles all the WAV header parsing for you. The I2S output then streams samples from the file in real time, so you are not loading the entire file into memory at once.

Button-triggered playback. The code uses edge detection (watching for the transition from True to False) rather than level detection (if not button.value) so each press triggers exactly one sound, not a continuous stream of triggers. Calling i2s.stop() before starting a new sound ensures clean interruption — pressing button 2 while button 1's sound is playing stops the first sound immediately.

Installing the libraries

audiocore and audiobusio are built into CircuitPython firmware for supported boards — no separate library installation is needed. If you see an import error, verify that your board's firmware version supports I2S output (most RP2040 and SAMD51 boards do).

Remix ideas

Remix idea

Trigger sounds from motion instead of buttons. The Motion Alarm project uses a PIR sensor — replace its LED alert with a play_sound() call to create an audio burglar alarm.

Remix idea

Add NeoPixel flashes synchronized to each sound. See NeoPixel Animations for the animation patterns, then fire an animation in play_sound() alongside the audio.

Remix idea

Outgrow the soundboard and build a generative synthesizer. The Euclidean Synthesizer project uses CircuitPython's synthio to create new sounds on the fly — no WAV files needed at all.

Go deeper

Credit: Adafruit Learning System