Asyncio-based Library for Buzzer-Music (CircuitPython)
January 16, 2025 ยท View on GitHub

Introduction
You can play simple melodies on a single or multiple passive buzzers using PWM. While the task of playing a simple tone is trivial (set the frequency and then the duty cycle on a PWM pin for the tone-duration), playing complete melodies is more challenging.
This repository provides some building blocks for playing even complex multi-tone melodies on a set of buzzers.
The project started off as a port of the MicroPython implementation from https://github.com/james1236/buzzer_music, but ended up as a complete new implementation makeing heavy use of asyncio.
Installation
The recommended way of installation is to use circup:
circup install -r requirements.txt
For a manual installation, copy the buzzer_music directory to your
lib directory of your device and install adafruit_ticks and
adafruit_asyncio.
Usage
The basic building block is the class AsyncBuzzer. This class can be
used directly and it is also used by higher level abstractions from
the library. See the examples simple_buzzer.py, poly_buzzer.py and
monophonic.py in the examples-directory.
The library also provides the class MusicPlayer which allows to play
complete works loading notes as necessary from a file:
async def main():
player = MusicPlayer(pins=[board.GP18,board.GP17,
board.GP15,board.GP13],debug=True)
# from: https://onlinesequencer.net/2244697
# needs four buzzers
print("playing 'Bach Prelude Nr.1 C-Major' at 80 bpm ...")
await player.play(filename="music/bach-prelude-C-Dur-4B.txt")
player.deinit()
asyncio.run(main())
The complete example is in examples/simple_player.py.
Notation
The MusicPlayer class expects the notes in a string or file in the
notation start pitch duration instrument; ..., e.g.:
song="4 D6 4 41;8 C6 4 41;12 F6 4 41;16 E6 8 41;..."
In a file, there must be a single note on a line and the lines must be sorted by start:
# "Happy Birthday"
bpm = 110
ref = 0.25
4 D6 4 41
8 C6 4 41
12 F6 4 41
16 E6 8 41
...
Comment lines starting with a '#' are ignored. The optional lines
bpm= and ref= define the beats per minute with reference (in
the example a quarter note for every beat).
The instrument-field and any other additional fields are ignored.
Onlinesequencer.net
The notation is not arbitrary. You can find many songs on https://onlinesequencer.net that are suitable for buzzers. Copying is a bit tricky: you have to enter edit mode (pencil button) and then select and copy all notes. Selecting all notes works either with 'CTRL-A' or with the select-all button, copying with 'CTRL-C'. Make sure that your cursor is somewhere above the score (but don't click into it) when using 'CTRL-A/CTRL-C'.

Once copied, insert the notes either directly into your program as a string (works for short pieces), or paste it into a file. In the first case, you have to remove the header (something like "Online Sequencer:250354:") and the trailing ':' manually.
When copying the notes into a file (the filename should not end in '.txt', otherwise there are no limitations), you just save it and then run a preprocessor script:
tools/preprocess-music.sh happy-birthday.raw
The example assumes that the notes were copyied into a file named
happy-birthday.raw. The preprocessor then splits the notes and
sorts them and finally writes the notes to happy-birthday.txt.
Implementation Notes
The player uses two async-tasks: one for reading notes and one for dispatching them to the buzzers. A third task does garbage-collection at a given interval. The reader and dispatcher tasks communicate using a double-ended queue (deque).
CircuitPython has an optimzed, dedicated class for deques in
collections.deque, but that class seems to have problems. This is
the reason that this library uses a simple list instead, replacing
deque.appendleft(x) with the less efficient
list.insert(0,x). Since the implementation limits the queue-size,
the performance penalty is small.
Asyncio relies on cooperative routines. This is important for music,
since the tone-durations and intervals should be as exact as
possible. Since the quality of music played on buzzers is very low
anyways, the implementation via asyncio is good enough in most
cases. Running MusicPlayer.play(...,debug=True) will output
timings. Some tests with a Pico and four buzzers show that it is no
problem to play 200-300 notes per minute without problems. Timing offsets
are in the range of a few milliseconds.
When using this library together with other code (e.g. LED-animations), take care not to block processing with long running tasks.
Tips and Tricks
Some random notes:
- Not every buzzer sounds the same, some are better than others.
- The frequency range of buzzers is limited. Very low frequencies tend to sum, high frequencies tend to peep.
MusicPlayerfails in dispatching notes if there are more notes than buzzers for a given timepoint. This is no problem if notes only overlap due to rounding effects, but it just does not work to play a melody and a full chord on only four buzzers. The constructor ofMusicPlayerhas a parameterskip=False. Settingskip=Truewill skip notes if all buzzers are busy (usually an inferior choice).- Onlinesequencer.net often provides multiple versions of a song. Simpler versions tend to sound better than complex, fat versions.
- Be aware of copyright issues when copying music.