Add monophonic Audio FX
This commit is contained in:
parent
8e74f2a28c
commit
bd80549170
19 changed files with 230 additions and 0 deletions
BIN
circuitpython-audio-fx/monophonic/T00.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T00.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T01RAND0.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T01RAND0.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T01RAND1.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T01RAND1.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T01RAND2.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T01RAND2.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T01RAND3.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T01RAND3.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T01RAND4.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T01RAND4.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T02.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T02.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T03.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T03.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T04HOLDL.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T04HOLDL.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T05NEXT0.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T05NEXT0.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T05NEXT1.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T05NEXT1.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T05NEXT2.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T05NEXT2.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T06LATCH.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T06LATCH.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T07.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T07.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T08.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T08.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T09.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T09.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T10.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T10.mp3
Normal file
Binary file not shown.
BIN
circuitpython-audio-fx/monophonic/T11HOLDL.mp3
Normal file
BIN
circuitpython-audio-fx/monophonic/T11HOLDL.mp3
Normal file
Binary file not shown.
230
circuitpython-audio-fx/monophonic/code.py
Normal file
230
circuitpython-audio-fx/monophonic/code.py
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue