Audio & music
Two ways to make sound on the PicoPad, both over its PWM audio pin. picogame_audio plays sample data (.wav files and generated tones) through a mixer so effects can overlap; picogame_synth generates tones in real time with synthio, so a whole SFX set costs almost no RAM. See /hardware/ for the audio pin and /reference/ for the bare signatures.
picogame_audio
Section titled “picogame_audio”A thin convenience layer over CircuitPython’s audio stack (audiopwmio + audiocore + audiomixer). Reach for it when you have .wav assets to play, or want simple beeps without bundling files. It sets up a mixer with several voices so a shot, an explosion, and looping music can all sound at once. The defaults (22050 Hz, mono, 16-bit signed) match typical ugame .wav assets - any sample you play must match that format.
Audio(pin=None, voices=4, sample_rate=22050, channels=1, bits=16, signed=True)
Section titled “Audio(pin=None, voices=4, sample_rate=22050, channels=1, bits=16, signed=True)”Constructs the audio output and starts the mixer playing immediately. pin=None uses board.AUDIO. voices is the number of simultaneous channels; voice 0 is reserved for music and voices 1..N-1 are used round-robin for sound effects. The other args define the sample format every clip must match.
load(path)- opens a.wavfile as a reusableWaveFilesample. Build it once and keep the returned object alive (it holds the open file); replaying it is cheap.play(sample, *, voice=None, loop=False, volume=1.0)- plays a sample.voiceis keyword-only;Nonepicks the next round-robin sfx voice.volumesets that voice’s level (0.0-1.0). Returns the voice index it used.sfx(sample, volume=1.0)- fire-and-forget effect on a free sfx voice (callsplaywithloop=False). Returns the voice index.music(sample, loop=True, volume=1.0)- plays on the reserved music voice (voice 0), looping by default.stop(voice=None)- stops one voice, or every voice ifvoiceisNone.stop_music()- stops just the music voice.is_playing(property) -Trueif any voice is currently playing.deinit()- releases the audio output. Theboard.AUDIOPWM pin is a singleton, so call this before constructing a secondAudio()- otherwise the nextAudio()raises pin in use.
tone(frequency=440, ms=120, sample_rate=22050, volume=0.6)
Section titled “tone(frequency=440, ms=120, sample_rate=22050, volume=0.6)”Builds a short square-wave RawSample in RAM - a beep with no .wav file needed. Good for prototyping and simple blips. The result matches a mono 16-bit signed mixer, so you can pass it straight to sfx/play.
import picogame_audioimport picogame_input
audio = picogame_audio.Audio() # PWM audio, 4 voicespew = picogame_audio.tone(880, 90) # high blip, built onceboom = picogame_audio.tone(140, 200) # low thud
btns = picogame_input.Buttons()while True: btns.poll() if btns.just_pressed(btns.A): audio.sfx(pew) # overlaps on a free voice if btns.just_pressed(btns.B): audio.sfx(boom, volume=0.8)Gotchas:
- Every sample must match the mixer’s format. A 44100 Hz or stereo
.wavwill play wrong (or error); resample assets to 22050 Hz mono 16-bit first. - Keep
load()results alive - if theWaveFileis garbage-collected the open file goes with it. Load clips once at startup, not per frame. - You only have
voices-1sfx channels; rapid-fire effects round-robin and will cut each other off once they wrap.
picogame_synth
Section titled “picogame_synth”Real-time synthesis via synthio - oscillators, ADSR envelopes, pitch-bend LFOs and low-pass filters, with no sample buffers. Reach for it when you want a rich SFX set or background music but cannot spare the RAM (or the asset files) that WAV samples need. It wraps the PWM-out -> mixer -> synthesizer chain and ships built-in waveforms. This is a device-only module: synthio is not in the simulator, so guard the import or only run it on hardware. For sample playback instead, use picogame_audio above; see /memory/ for why the RAM savings matter.
Built-in waveform constants (one-cycle, signed 16-bit arrays you share across notes): SINE, SAW, TRIANGLE, SQUARE, NOISE. The functions sine(), saw(), triangle(), square(), noise() build fresh copies if you need them.
note(midi, waveform=None, attack=0.005, decay=0.06, sustain=0.0, release=0.08, amplitude=0.6, bend=None, cutoff=None)
Section titled “note(midi, waveform=None, attack=0.005, decay=0.06, sustain=0.0, release=0.08, amplitude=0.6, bend=None, cutoff=None)”Builds a reusable note/SFX/instrument - the core building block. midi is a MIDI note number (60 = middle C, 72 = C5). waveform is one of the constants above. attack/decay/sustain/release shape the ADSR envelope in seconds (a short decay with sustain=0.0 gives a percussive blip). amplitude is loudness (0.0-1.0). bend takes a pitch_bend LFO for a slide; cutoff adds a low-pass filter at that many Hz to round off harsh tones. Build each note once and replay it.
pitch_bend(semitones, ms, waveform=None, once=True)
Section titled “pitch_bend(semitones, ms, waveform=None, once=True)”Returns a synthio.LFO suitable for a note’s bend - it slides the pitch by semitones over ms milliseconds. Use a positive value for a rising zap, negative for a falling drop. With once=True it runs the slide a single time per trigger.
Synth(pin=None, sample_rate=22050, buffer_size=2048, music_level=0.4, sfx_level=0.7)
Section titled “Synth(pin=None, sample_rate=22050, buffer_size=2048, music_level=0.4, sfx_level=0.7)”Sets up PWM out and a 2-voice mixer: voice 0 for music (a MidiTrack), voice 1 for the live synth used by SFX. pin=None uses board.AUDIO. music_level/sfx_level are the starting mix levels for those two voices.
sfx(n)- plays notenas a one-shot effect. It retriggers the note’s LFOs first (so a repeated zap sounds identical every time), then callsrelease_all_then_press, so back-to-back SFX cut cleanly.press(n)/release(n)- hold and release a note manually, for sounds that last as long as a button is held rather than firing once.music(midi_track)- plays aMidiTrack(fromload_midi) on voice 0, looping.stop_music()- stops the music voice.
load_midi(path, sample_rate=22050, waveform=None, envelope=None, tempo=120, ppqn=240)
Section titled “load_midi(path, sample_rate=22050, waveform=None, envelope=None, tempo=120, ppqn=240)”Loads a .mid file as a synthio.MidiTrack to feed to Synth.music. waveform and envelope pick the instrument voice for the whole track; tempo (BPM) and ppqn set playback speed. It auto-skips a standard format-0 SMF header so a plain exported MIDI just works.
import picogame_synth as sndimport picogame_input
s = snd.Synth()# Each SFX is a Note built ONCE (envelope + optional bend + filter).blip = snd.note(72, snd.SQUARE, decay=0.10)zap = snd.note(60, snd.SAW, decay=0.18, cutoff=4000, bend=snd.pitch_bend(12, 180))boom = snd.note(45, snd.NOISE, attack=0.0, decay=0.30, amplitude=0.55, cutoff=2800)
btn = picogame_input.Buttons()while True: btn.poll() if btn.just_pressed(btn.A): s.sfx(zap)Gotchas:
- Device only - synthio is absent in the simulator. Import
picogame_synthinside anif/tryor only on real hardware, the way the train and dinorun examples do. - Build each
note()once at startup and replay it;sfxhandles retriggering. Rebuilding notes per frame wastes time and breaks LFO retrigger. - It is the synthesizer that makes sound, not the note object - effects only play through a live
Synth, and there are just two mixer voices (one music, one SFX), so overlapping synth SFX share a single voice.