Adafruit_Learning_System_Gu.../circuitpython-audio-fx/monophonic/code.py

230 lines
5.4 KiB
Python

# 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