diff --git a/circuitpython-audio-fx/monophonic/T00.mp3 b/circuitpython-audio-fx/monophonic/T00.mp3 new file mode 100644 index 000000000..c1251c01a Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T00.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T01RAND0.mp3 b/circuitpython-audio-fx/monophonic/T01RAND0.mp3 new file mode 100644 index 000000000..32a7548c1 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T01RAND0.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T01RAND1.mp3 b/circuitpython-audio-fx/monophonic/T01RAND1.mp3 new file mode 100644 index 000000000..39aff6f00 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T01RAND1.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T01RAND2.mp3 b/circuitpython-audio-fx/monophonic/T01RAND2.mp3 new file mode 100644 index 000000000..bfd682216 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T01RAND2.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T01RAND3.mp3 b/circuitpython-audio-fx/monophonic/T01RAND3.mp3 new file mode 100644 index 000000000..153c76291 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T01RAND3.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T01RAND4.mp3 b/circuitpython-audio-fx/monophonic/T01RAND4.mp3 new file mode 100644 index 000000000..63032f479 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T01RAND4.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T02.mp3 b/circuitpython-audio-fx/monophonic/T02.mp3 new file mode 100644 index 000000000..5d0730c4c Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T02.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T03.mp3 b/circuitpython-audio-fx/monophonic/T03.mp3 new file mode 100644 index 000000000..501ebb912 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T03.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T04HOLDL.mp3 b/circuitpython-audio-fx/monophonic/T04HOLDL.mp3 new file mode 100644 index 000000000..b65ff78e6 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T04HOLDL.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T05NEXT0.mp3 b/circuitpython-audio-fx/monophonic/T05NEXT0.mp3 new file mode 100644 index 000000000..da73c7173 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T05NEXT0.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T05NEXT1.mp3 b/circuitpython-audio-fx/monophonic/T05NEXT1.mp3 new file mode 100644 index 000000000..d81ecc95f Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T05NEXT1.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T05NEXT2.mp3 b/circuitpython-audio-fx/monophonic/T05NEXT2.mp3 new file mode 100644 index 000000000..245cc9e42 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T05NEXT2.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T06LATCH.mp3 b/circuitpython-audio-fx/monophonic/T06LATCH.mp3 new file mode 100644 index 000000000..8cb950da4 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T06LATCH.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T07.mp3 b/circuitpython-audio-fx/monophonic/T07.mp3 new file mode 100644 index 000000000..d32870566 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T07.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T08.mp3 b/circuitpython-audio-fx/monophonic/T08.mp3 new file mode 100644 index 000000000..bd85b2cc3 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T08.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T09.mp3 b/circuitpython-audio-fx/monophonic/T09.mp3 new file mode 100644 index 000000000..e64b560b3 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T09.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T10.mp3 b/circuitpython-audio-fx/monophonic/T10.mp3 new file mode 100644 index 000000000..db411a8a7 Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T10.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/T11HOLDL.mp3 b/circuitpython-audio-fx/monophonic/T11HOLDL.mp3 new file mode 100644 index 000000000..c1251c01a Binary files /dev/null and b/circuitpython-audio-fx/monophonic/T11HOLDL.mp3 differ diff --git a/circuitpython-audio-fx/monophonic/code.py b/circuitpython-audio-fx/monophonic/code.py new file mode 100644 index 000000000..efa6e59a4 --- /dev/null +++ b/circuitpython-audio-fx/monophonic/code.py @@ -0,0 +1,230 @@ +# 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