retrobeep
retrobeep is a standalone MicroPython C module for simple PWM music playback on RP2040-class hardware. The source file is named pbeep.c, but the registered user-facing module name is retrobeep.
This module is independent of retroPy, but it fits well into retroPy projects that need lightweight music playback.
Import
import retrobeep
Overview
The implementation visible in this source currently provides:
- 2 independent tracks
- 4 voices of polyphony per track
- PWM output on two pins
- a fixed sample rate of
22050 - note scheduling by step index
- per-track play / pause / stop
- per-track mute and loop controls
- sequence loading from Python tuples or
.beepfiles
Waveforms currently visible in the audio mixer:
0= square1= triangle2= sine
The note system uses MIDI note numbers. The sequencer accepts:
- valid note range:
12to120 255for rest / no note
Output pins
Default PWM pins in the source are:
- channel A: GPIO
3 - channel B: GPIO
4
You can change them before starting playback.
Basic control
retrobeep.start()
Initializes PWM output, resets internal state, and starts the audio worker thread.
retrobeep.stop()
Stops output by disabling PWM, zeroing the channel levels, and returning the pins to GPIO input mode.
retrobeep.reset_all()
Clears all sequences, resets all voices, resets per-track step/state, and silences the PWM outputs.
Sequence formats
retrobeep.set_sequence(track, sequence)
Loads a sequence from a Python list of 4-tuples.
Each tuple is:
(start_step, midi_note, duration_steps, instrument)
Example:
retrobeep.set_sequence(0, [
(0, 60, 4, 0),
(4, 64, 4, 0),
(8, 67, 4, 0),
])
Behavior visible in the source:
- track index must be
0or1 - the sequence length is capped by
MAX_SEQUENCE - loading a new sequence clears currently active voices for that track
loop_endis auto-computed from the last note end time
retrobeep.load_beep(track, source)
Loads a sequence from a .beep binary source.
The source may be:
- a filename string
- a file-like object with
.read()
The file format expected by this source begins with a two-byte header:
'B''P'
After that, notes are read in 4-byte records:
[start][note_id][duration][instrument]
Tempo
retrobeep.set_tempo(bpm, steps_per_beat)
Sets the internal sequencer tick rate.
The source computes:
retrobeep_ticks_per_second = (bpm * steps_per_beat) / 60
This means the second argument is the sequencer density, such as 4, 8, 16, and so on.
Example:
retrobeep.set_tempo(120, 4)
Pins
retrobeep.set_pins(pin_a, pin_b)
Changes the two PWM output pins.
Important: the source raises an error if you try to change pins while PWM is already running.
Per-track control
retrobeep.play_channel(track)
Puts a track into the playing state. If the track was stopped, the play position is rewound to step 0.
retrobeep.pause_channel(track)
Pauses a track without clearing its position.
retrobeep.stop_channel(track)
Stops a track, rewinds it to step 0, and clears its voices.
retrobeep.mute_channel(track[, state])
Mutes or unmutes a track.
- with one argument, it toggles mute
- with two arguments, it explicitly sets mute to
TrueorFalse
retrobeep.loop_channel(track, enable[, loop_len_steps])
Enables or disables looping for a track.
If loop_len_steps is omitted, the module uses the auto-computed loop_end from the sequence. If supplied, it overrides the loop boundary.
Minimal example
import retrobeep
retrobeep.set_pins(3, 4)
retrobeep.set_tempo(120, 4)
retrobeep.set_sequence(0, [
(0, 60, 4, 0),
(4, 64, 4, 0),
(8, 67, 4, 0),
(12, 72, 4, 0),
])
retrobeep.loop_channel(0, True)
retrobeep.start()
.beep loading example
import retrobeep
retrobeep.load_beep(0, "theme.beep")
retrobeep.loop_channel(0, True)
retrobeep.start()
Notes and cautions
- The source registers the module name as
retrobeep, notpbeep. - The module is RP2040 / PWM oriented and not a general desktop audio backend.
- The visible worker loop in this file runs continuously in a background thread.
- In the current source,
stop()should be understood as disable / silence output. Treat the implementation as low-level and evolving. - The mixer advances oscillator phase only while a track is in the playing state.
- Looping clears leftover voices at the wrap point to avoid overlap across loop boundaries.