Skip to content

Add monophonic Audio FX #2910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added circuitpython-audio-fx/monophonic/T00.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T01RAND0.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T01RAND1.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T01RAND2.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T01RAND3.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T01RAND4.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T02.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T03.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T04HOLDL.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T05NEXT0.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T05NEXT1.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T05NEXT2.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T06LATCH.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T07.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T08.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T09.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T10.mp3
Binary file not shown.
Binary file added circuitpython-audio-fx/monophonic/T11HOLDL.mp3
Binary file not shown.
230 changes: 230 additions & 0 deletions circuitpython-audio-fx/monophonic/code.py
Original file line number Diff line number Diff line change
@@ -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