Polyphonic Audio FX is pretty cool, but suppose you have an existing project for the original Audio FX and you want to "port" it to a newer board using CircuitPython instead of VS1000. We got you! Load up your CircuitPython with the monophonic version of the script and you'll get behavior a lot closer to the VS1000-based Audio FX boards. (not working quite right? Please drop us some feedback using the link in the sidebar!)
Wiring is the same as shown under Using CircuitPython Audio FX.
Convert your OGG files to MP3 and save to CIRCUITPY
Using software of your choice, such as the free and open source Audacity which is available on Linux, Windows & Mac computers. Open each file and then export it as an MP3 file. Keep the first part of the filename (e.g., T01RAND0 or T11LATCH) but choose the right file extension (.mp3)
The RP2350 has approximately 3MB of flash available for the CIRCUITPY drive, so if you were using a VS1000 with a larger capacity you will have to choose a higher compression ratio (lower bit rate) when performing the conversion.
If you still have original wave files with a .WAV extension, it's better to use these files for MP3 conversion, because the WAV file is lossless. Doing repeated lossy audio conversions can reduce the quality of the final audio file, the audio version of those weird crinkly edges and smudgy indistinct areas that JPEG files can get.
Unlike the polyphonic version, your files can have different sample rates, you can mix mono & stereo, etc. For more information about naming and converting files see the headings "Preparing Audio Files" and "Naming Audio Files" under Using CircuitPython Audio FX. For information about how each trigger type works, read about it in the original Audio FX guide.
Coding CircuitPython Audio FX
Click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: Copyright 2024 Jeff Epler for Adafruit Industries # SPDX-License-Identifier: MIT # pylint: disable=no-self-use import os import io import random import board import digitalio import keypad import audiobusio import audiocore import audiomp3 # Configure the pins to use -- earlier in list = higher priority pads = [ board.GP0, board.GP1, board.GP2, board.GP3, board.GP4, board.GP5, board.GP6, board.GP7, board.GP8, board.GP9, board.GP10, board.GP11, board.GP12, board.GP13, board.GP14, board.GP15 ] # Configure the audio device audiodev = audiobusio.I2SOut( bit_clock=board.GP16, word_select=board.GP17, data=board.GP18 ) led = digitalio.DigitalInOut(board.LED) led.switch_to_output(False) # This is enough to register as an MP3 file with mp3decoder!, allows creating a decoder # without "opening" a "file"! EMPTY_MP3_BYTES = b"\xff\xe3" # Create the MP3 decoder object decoder = audiomp3.MP3Decoder(io.BytesIO(EMPTY_MP3_BYTES)) def exists(p): try: os.stat(p) return True except OSError: return False def random_shuffle(seq): for i in range(len(seq)): j = random.randrange(0, i+1) if i != j: # Chance an item remains in same location seq[i], seq[j] = seq[j], seq[i] def random_cycle(seq): while True: random_shuffle(seq) yield from seq def cycle(seq): while True: yield from seq class TriggerBase: def __init__(self, prefix): self._filenames = list(self._gather_filenames(prefix)) self._filename_generator = type(self).generate_filenames(self._filenames) self.wants_to_play = False # Can be cycle or random_cycle generate_filenames = cycle def on_press(self): self.wants_to_play = True def on_release(self): self.wants_to_play = False def on_activate(self): self.play_wait() def _gather_filenames(self, prefix): if self.stems is None: return for stem in self.stems: name_mp3 = f"{prefix}{stem}.mp3" if exists(name_mp3): yield name_mp3 continue name_wav = f"{prefix}{stem}.wav" if exists(name_wav): yield name_wav continue def _get_sample(self, path): if path.endswith(".mp3"): decoder.open(path) return decoder else: return audiocore.WaveFile(path) def play(self, loop=False): audiodev.stop() path = next(self._filename_generator) sample = self._get_sample(path) audiodev.play(sample, loop=loop) def play_wait(self): self.play() while audiodev.playing: poll_keys() def stop(self): audiodev.stop() @classmethod def matches(cls, prefix): stem = cls.stems[0] name_mp3 = f"{prefix}{stem}.mp3" name_wav = f"{prefix}{stem}.wav" return exists(name_wav) or exists(name_mp3) def __repr__(self): return (f"<{self.__class__.__name__} {' '.join(self._filenames)}" + f"{' ACTIVE' if self.wants_to_play else ''}>") class NopTrigger(TriggerBase): """Does nothing.""" stems = None def on_activate(self): return class BasicTrigger(TriggerBase): """Plays a file each time the button is pressed down""" stems = [""] class HoldLoopingTrigger(TriggerBase): """Plays a file as long as a button is held down This differs from the basic trigger because the loop stops as soon as the button is released """ stems = ["HOLDL"] def on_activate(self): self.play(loop=True) while audiodev.playing: poll_keys() for trigger in triggers: if trigger is self: break if trigger.wants_to_play: self.wants_to_play = False if not self.wants_to_play: audiodev.stop() class LatchingLoopTrigger(HoldLoopingTrigger): """Plays a file until the button is pressed again When the button is pressed again, stops the loop immediately.""" stems = ["LATCH"] def on_press(self): if self.wants_to_play or not audiodev.playing: self.wants_to_play = not self.wants_to_play def on_release(self): pass # override default behavior class PlayNextTrigger(TriggerBase): stems = [f"NEXT{i}" for i in range(10)] _phase = 0 class PlayRandomTrigger(TriggerBase): stems = [f"RAND{i}" for i in range(10)] generate_filenames = random_cycle trigger_classes = [ BasicTrigger, HoldLoopingTrigger, LatchingLoopTrigger, PlayNextTrigger, PlayRandomTrigger, ] def make_trigger(i): prefix = f"T{i:02d}" for cls in trigger_classes: if not cls.matches(prefix): continue return cls(prefix) return NopTrigger(prefix) keys = keypad.Keys(pads, value_when_pressed=False) triggers = [make_trigger(i) for i in range(len(pads))] def poll_keys(): while e := keys.events.get(): trigger = triggers[e.key_number] if e.pressed: trigger.on_press() else: trigger.on_release() print(e.pressed, trigger) print(triggers) reversed_triggers = list(reversed(triggers)) while True: poll_keys() for t in triggers: if t.wants_to_play: print(t) t.on_activate() break
Upload the Code, Sound Effects and Libraries to the Raspberry Pi Pico 2
After downloading the Project Bundle, plug your Pico 2 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Open up the zipped file, go to the the appropriate folder inside (e.g., circuitpython-audio-fx/polyphonic/CircuitPython 9.x if you are using CircuitPython 9.x) and copy the items in that folder directly onto your CIRCUITPY drive.
Your CIRCUITPY drive should look like this after copying the sound effects and the code.py file.
At this point, CircuitPython will automatically restart and run the Audio FX code. If you activate the digital inputs (for instance, by pressing a connected button) a sound effect will play. If you run into trouble, you can use the CircuitPython serial connection, sometimes call the REPL, to troubleshoot.
Text editor powered by tinymce.