Audio & music
picogame offers four audio layers. picogame_audioout picks the board’s output device (PWM or I2S DAC).
picogame_audio mixes .wav files and generated PCM tones. picogame_synth creates notes in real time
with synthio and avoids a PCM buffer for every effect. picogame_sfx adds a ready-made set of named game
sounds over the synthesizer. See /hardware/ for board audio setup and
/reference/ for signatures.
picogame_audioout — one output for every board
Section titled “picogame_audioout — one output for every board”picogame_audio and picogame_synth both get their output device from picogame_audioout.make_output(),
so a game needs no board-specific audio code. It auto-selects:
- an I2S DAC (e.g. the Fruit Jam’s TLV320) when the board exposes
board.I2S_BCLK, - otherwise a PWM output on the board’s speaker pin (or a pin you pass).
You normally never call it directly — construct Audio() / Synth() and it happens for you. make_output(sample_rate=22050, pin=None) is there if you want the raw device; passing an explicit pin forces PWM.
Volume (I2S DAC boards): the TLV320 driver’s defaults are deliberately very quiet, so on a Fruit Jam sound can seem absent until you raise them in settings.toml — PICOGAME_AUDIO_OUT (headphone/speaker/both), PICOGAME_DAC_VOLUME, PICOGAME_HP_VOLUME, PICOGAME_SPK_VOLUME (integer dB, keep <= 0). See the settings.toml reference.
picogame_audio
Section titled “picogame_audio”A convenience layer over CircuitPython’s audio stack (audiocore + audiomixer) on top of the
picogame_audioout output. 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 lets picogame_audioout pick the device (I2S DAC or the board’s PWM speaker pin); an explicit pin forces PWM on that pin. 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. The output device is a singleton, so call this before constructing a secondAudio()(or aSynth()sharing the pin) - otherwise the next one 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. 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)picogame_synth
Section titled “picogame_synth”picogame_synth wraps synthio oscillators, ADSR envelopes, pitch-bend LFOs, and low-pass filters.
It avoids storing a PCM buffer for every effect, but still allocates the audio output buffer, mixer,
waveforms, and note objects. Use it for a larger SFX set or MIDI music when WAV assets would consume
too much RAM or flash.
The module is safe to import on firmware without audio. If audio initialisation fails, its API becomes
a silent no-op; check AVAILABLE or synth.available only when the UI needs to expose audio settings.
The desktop simulator’s live pygame window plays these picogame_synth voices through pygame.mixer (an approximation for tuning by ear); headless runs stay silent. Either way the same game path runs unchanged.
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. For a crisp arcade blip reach for a short SQUARE note (SINE and TRIANGLE read softer and rounder).
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 pitch sweep (a quick wobble; see pitch_bend); 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 for a note’s bend. With once=True it is a one-shot sine sweep: over ms the pitch swings toward semitones and back - a wobble/swoop, not a clean monotonic glide. Keep ms short (about the note’s attack+decay) so mostly the rise (positive = zap up) or fall (negative = drop) is heard; a long ms lets the return swing show and sounds wobbly.
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 the output (via picogame_audioout — I2S DAC or PWM, same as Audio) and a 2-voice mixer: voice 0 for music (a MidiTrack), voice 1 for the live synth used by SFX. pin=None auto-selects the device; an explicit pin forces PWM. 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.
Drone(synth, waveform=None, amplitude=0.35, attack=0.03, release=0.12)
Section titled “Drone(synth, waveform=None, amplitude=0.35, attack=0.03, release=0.12)”A continuously held note for engine, siren, or drone-style sounds - press it once, then steer its pitch/volume live each frame. Where note()/sfx fire a one-shot, a Drone keeps a single note sounding on the SFX voice; synthio reads the note’s live .frequency/.amplitude per audio buffer, so the tone tracks whatever you feed it (e.g. an engine note driven by car speed). synth is a live Synth; waveform defaults to SAW.
.start()- press the held note (idempotent - safe to call again while playing)..set(frequency, amplitude=None)- update the live pitch (Hz) every frame, and optionally the volume..stop()- release the note (e.g. on the title or results screen).
eng = snd.Drone(s, waveform=snd.SAW)eng.start() # at race start# each frame, rev = speed / max_speed in 0..1:eng.set(70 + 270 * rev, amplitude=0.2 + 0.5 * rev)eng.stop() # on the title / results screenload_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 for Synth.music. waveform and envelope select
the instrument voice for the track; tempo (BPM) and ppqn set playback speed. The loader accepts
a standard format-0 SMF header.
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)picogame_sfx
Section titled “picogame_sfx”picogame_sfx provides one cohesive set of named effects over picogame_synth. Use it when the
built-in arcade-style sounds fit the game. Build custom picogame_synth.note() objects when the kit
does not cover an event or the game needs a different audio identity. With no audio, every call is a
silent no-op.
Kit(synth)
Section titled “Kit(synth)”Build the theme’s voices once from a live picogame_synth.Synth(), then fire effects by name (and call tick() once per frame). Each call maps to a game event; the kit arbitrates them by priority through the single SFX voice, so a big scene sound briefly holds off the small feedback blips instead of being cut by them.
| Call | Fires on | Preview |
|---|---|---|
blip() |
UI tick — menu move, select, confirm | |
coin() |
a pickup / gained value | |
powerup() |
level / wave clear, upgrade | |
zap() |
your shot or dash | |
pew() |
an enemy fired | |
jump() |
a jump / launch | |
hit() |
a hit landed on an enemy | |
hurt() |
you took damage | |
boom() |
something destroyed / ended | |
explosion() |
a big scene blast |
hit() varies its brightness across a rapid burst so repeats don’t grate; pass rotate=False for identical hits. Call tick() once per frame — it plays out the coin/powerup arpeggios and counts down the protected windows.
Volume/mute live on the Synth: synth.set_levels(music=None, sfx=None) sets either mixer level (0.0-1.0), and synth.mute(on) silences everything while remembering the levels. Wire these to a picogame_options menu + picogame_save for a persisted volume control.
import picogame_synth as sndimport picogame_sfx as sfximport picogame_input
s = snd.Synth()kit = sfx.Kit(s) # builds the signature voices once (silent no-op if no audio)
btn = picogame_input.Buttons()while True: btn.poll() if btn.just_pressed(btn.A): kit.zap() # your shot if btn.just_pressed(btn.B): kit.jump() # ... on a kill: kit.boom(); on taking damage: kit.hurt(); on a pickup: kit.coin() kit.tick() # once per frame - drives sequences + priority windows