Skip to content

Commit 41f3f0f

Browse files
authored
Merge pull request #2910 from jepler/circuitpython-audiofx-monophonic
Add monophonic Audio FX
2 parents 1c4b493 + bd80549 commit 41f3f0f

19 files changed

+230
-0
lines changed
21.9 KB
Binary file not shown.
10.1 KB
Binary file not shown.
9.7 KB
Binary file not shown.
9.42 KB
Binary file not shown.
9.98 KB
Binary file not shown.
9.7 KB
Binary file not shown.
3.8 KB
Binary file not shown.
4.22 KB
Binary file not shown.
6.05 KB
Binary file not shown.
6.89 KB
Binary file not shown.
6.75 KB
Binary file not shown.
5.91 KB
Binary file not shown.
6.61 KB
Binary file not shown.
4.36 KB
Binary file not shown.
3.66 KB
Binary file not shown.
4.36 KB
Binary file not shown.
3.8 KB
Binary file not shown.
21.9 KB
Binary file not shown.
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# SPDX-FileCopyrightText: Copyright 2024 Jeff Epler for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
# pylint: disable=no-self-use
5+
6+
import os
7+
import io
8+
import random
9+
10+
import board
11+
import digitalio
12+
import keypad
13+
import audiobusio
14+
import audiocore
15+
import audiomp3
16+
17+
# Configure the pins to use -- earlier in list = higher priority
18+
pads = [
19+
board.GP0, board.GP1, board.GP2, board.GP3,
20+
board.GP4, board.GP5, board.GP6, board.GP7,
21+
board.GP8, board.GP9, board.GP10, board.GP11,
22+
board.GP12, board.GP13, board.GP14, board.GP15
23+
]
24+
25+
# Configure the audio device
26+
audiodev = audiobusio.I2SOut(
27+
bit_clock=board.GP16, word_select=board.GP17, data=board.GP18
28+
)
29+
30+
led = digitalio.DigitalInOut(board.LED)
31+
led.switch_to_output(False)
32+
33+
# This is enough to register as an MP3 file with mp3decoder!, allows creating a decoder
34+
# without "opening" a "file"!
35+
EMPTY_MP3_BYTES = b"\xff\xe3"
36+
37+
# Create the MP3 decoder object
38+
decoder = audiomp3.MP3Decoder(io.BytesIO(EMPTY_MP3_BYTES))
39+
40+
def exists(p):
41+
try:
42+
os.stat(p)
43+
return True
44+
except OSError:
45+
return False
46+
47+
48+
def random_shuffle(seq):
49+
for i in range(len(seq)):
50+
j = random.randrange(0, i+1)
51+
if i != j: # Chance an item remains in same location
52+
seq[i], seq[j] = seq[j], seq[i]
53+
54+
def random_cycle(seq):
55+
while True:
56+
random_shuffle(seq)
57+
yield from seq
58+
59+
def cycle(seq):
60+
while True:
61+
yield from seq
62+
63+
class TriggerBase:
64+
def __init__(self, prefix):
65+
self._filenames = list(self._gather_filenames(prefix))
66+
self._filename_generator = type(self).generate_filenames(self._filenames)
67+
self.wants_to_play = False
68+
69+
# Can be cycle or random_cycle
70+
generate_filenames = cycle
71+
72+
def on_press(self):
73+
self.wants_to_play = True
74+
75+
def on_release(self):
76+
self.wants_to_play = False
77+
78+
def on_activate(self):
79+
self.play_wait()
80+
81+
def _gather_filenames(self, prefix):
82+
if self.stems is None:
83+
return
84+
for stem in self.stems:
85+
name_mp3 = f"{prefix}{stem}.mp3"
86+
if exists(name_mp3):
87+
yield name_mp3
88+
continue
89+
name_wav = f"{prefix}{stem}.wav"
90+
if exists(name_wav):
91+
yield name_wav
92+
continue
93+
94+
def _get_sample(self, path):
95+
if path.endswith(".mp3"):
96+
decoder.open(path)
97+
return decoder
98+
else:
99+
return audiocore.WaveFile(path)
100+
101+
def play(self, loop=False):
102+
audiodev.stop()
103+
path = next(self._filename_generator)
104+
sample = self._get_sample(path)
105+
audiodev.play(sample, loop=loop)
106+
107+
def play_wait(self):
108+
self.play()
109+
while audiodev.playing:
110+
poll_keys()
111+
112+
def stop(self):
113+
audiodev.stop()
114+
115+
@classmethod
116+
def matches(cls, prefix):
117+
stem = cls.stems[0]
118+
name_mp3 = f"{prefix}{stem}.mp3"
119+
name_wav = f"{prefix}{stem}.wav"
120+
return exists(name_wav) or exists(name_mp3)
121+
122+
def __repr__(self):
123+
return (f"<{self.__class__.__name__} {' '.join(self._filenames)}" +
124+
f"{' ACTIVE' if self.wants_to_play else ''}>")
125+
126+
127+
class NopTrigger(TriggerBase):
128+
"""Does nothing."""
129+
130+
stems = None
131+
132+
def on_activate(self):
133+
return
134+
135+
class BasicTrigger(TriggerBase):
136+
"""Plays a file each time the button is pressed down"""
137+
138+
stems = [""]
139+
140+
class HoldLoopingTrigger(TriggerBase):
141+
"""Plays a file as long as a button is held down
142+
143+
This differs from the basic trigger because the loop stops as soon as the button
144+
is released """
145+
146+
stems = ["HOLDL"]
147+
148+
def on_activate(self):
149+
self.play(loop=True)
150+
while audiodev.playing:
151+
poll_keys()
152+
for trigger in triggers:
153+
if trigger is self:
154+
break
155+
if trigger.wants_to_play:
156+
self.wants_to_play = False
157+
if not self.wants_to_play:
158+
audiodev.stop()
159+
160+
class LatchingLoopTrigger(HoldLoopingTrigger):
161+
"""Plays a file until the button is pressed again
162+
163+
When the button is pressed again, stops the loop immediately."""
164+
165+
stems = ["LATCH"]
166+
167+
def on_press(self):
168+
if self.wants_to_play or not audiodev.playing:
169+
self.wants_to_play = not self.wants_to_play
170+
171+
def on_release(self):
172+
pass # override default behavior
173+
174+
175+
class PlayNextTrigger(TriggerBase):
176+
stems = [f"NEXT{i}" for i in range(10)]
177+
_phase = 0
178+
179+
180+
class PlayRandomTrigger(TriggerBase):
181+
stems = [f"RAND{i}" for i in range(10)]
182+
183+
generate_filenames = random_cycle
184+
185+
186+
187+
trigger_classes = [
188+
BasicTrigger,
189+
HoldLoopingTrigger,
190+
LatchingLoopTrigger,
191+
PlayNextTrigger,
192+
PlayRandomTrigger,
193+
]
194+
195+
196+
def make_trigger(i):
197+
prefix = f"T{i:02d}"
198+
199+
for cls in trigger_classes:
200+
if not cls.matches(prefix):
201+
continue
202+
return cls(prefix)
203+
204+
return NopTrigger(prefix)
205+
206+
207+
keys = keypad.Keys(pads, value_when_pressed=False)
208+
209+
triggers = [make_trigger(i) for i in range(len(pads))]
210+
211+
def poll_keys():
212+
while e := keys.events.get():
213+
trigger = triggers[e.key_number]
214+
if e.pressed:
215+
trigger.on_press()
216+
else:
217+
trigger.on_release()
218+
print(e.pressed, trigger)
219+
220+
print(triggers)
221+
222+
reversed_triggers = list(reversed(triggers))
223+
224+
while True:
225+
poll_keys()
226+
for t in triggers:
227+
if t.wants_to_play:
228+
print(t)
229+
t.on_activate()
230+
break

0 commit comments

Comments
 (0)