Skip to content

Commit a54bb0f

Browse files
authored
Merge pull request #70 from dcbriccetti/tilt-instrument
Create Tilting Arpeggios
2 parents d87ea26 + b6a8e5f commit a54bb0f

File tree

1 file changed

+131
-0
lines changed

1 file changed

+131
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""Tilting Arpeggios
2+
3+
This program plays notes from arpeggios in a circle of fourths. Y-axis tilt chooses the note.
4+
Buttons A and B advance forward and backward through the circle. The switch selects
5+
the type of arpeggio, either dominant seventh or blues.
6+
7+
You can ignore the FrequencyProvider class if you’re just interested in the CPX interface.
8+
9+
See a code walkthrough here: https://www.youtube.com/watch?v=cDhqyT3ZN0g
10+
"""
11+
12+
# pylint: disable=R0903
13+
import time
14+
from adafruit_circuitplayground.express import cpx
15+
16+
HS_OCT = 12 # Half-steps per octave
17+
HS_4TH = 5 # Half-steps in a fourth
18+
ARPEGGIOS = (
19+
(0, 4, 7, 10), # Dominant seventh
20+
(0, 3, 5, 6, 7, 10)) # Blues
21+
NUM_OCTAVES = 2
22+
STARTING_NOTE = 233.08
23+
MIN_NOTE_PLAY_SECONDS = 0.25
24+
BUTTON_REPEAT_AFTER_SECONDS = 0.25
25+
26+
27+
class FrequencyMaker:
28+
"""Provide frequencies for playing notes"""
29+
def __init__(self):
30+
num_octaves_to_pre_compute = NUM_OCTAVES + 2
31+
num_freqs = HS_OCT * num_octaves_to_pre_compute
32+
33+
def calc_freq(i):
34+
return STARTING_NOTE * 2 ** (i / HS_OCT)
35+
36+
self.note_frequencies = [calc_freq(i) for i in range(num_freqs)]
37+
self.arpeg_note_indexes = FrequencyMaker.create_arpeggios(num_octaves_to_pre_compute)
38+
self.circle_pos = 0
39+
self.key_offset = 0
40+
41+
@staticmethod
42+
def create_arpeggios(num_octaves):
43+
"""Create a list of arpeggios, where each one is a list of chromatic scale note indexes"""
44+
return [FrequencyMaker.create_arpeggio(arpeggio, num_octaves) for arpeggio in ARPEGGIOS]
45+
46+
@staticmethod
47+
def create_arpeggio(arpeggio, num_octaves):
48+
return [octave * HS_OCT + note for octave in range(num_octaves) for note in arpeggio]
49+
50+
def advance(self, amount):
51+
"""Advance forward or backward through the circle of fourths"""
52+
self.circle_pos = (self.circle_pos + amount) % HS_OCT
53+
self.key_offset = self.circle_pos * HS_4TH % HS_OCT
54+
55+
def freq(self, normalized_position, selected_arpeg):
56+
"""Return the frequency for the note at the specified position in the specified arpeggio"""
57+
selected_arpeg_note_indexes = self.arpeg_note_indexes[selected_arpeg]
58+
num_notes_in_selected_arpeg = len(ARPEGGIOS[selected_arpeg])
59+
num_arpeg_notes_in_range = num_notes_in_selected_arpeg * NUM_OCTAVES + 1
60+
arpeg_index = int(normalized_position * num_arpeg_notes_in_range)
61+
note_index = self.key_offset + selected_arpeg_note_indexes[arpeg_index]
62+
return self.note_frequencies[note_index]
63+
64+
65+
class ButtonDetector:
66+
def __init__(self):
67+
self.next_press_allowed_at = time.monotonic()
68+
self.buttons_on = (cpx.button_a, cpx.button_b)
69+
70+
def pressed(self, index):
71+
"""Return whether the specified button (0=A, 1=B) was pressed, limiting the repeat rate"""
72+
pressed = cpx.button_b if index else cpx.button_a
73+
if pressed:
74+
now = time.monotonic()
75+
if now >= self.next_press_allowed_at:
76+
self.next_press_allowed_at = now + BUTTON_REPEAT_AFTER_SECONDS
77+
return True
78+
return False
79+
80+
81+
class TiltingArpeggios:
82+
def __init__(self):
83+
cpx.pixels.brightness = 0.2
84+
self.freq_maker = FrequencyMaker()
85+
TiltingArpeggios.update_pixel(self.freq_maker.circle_pos)
86+
self.button = ButtonDetector()
87+
self.last_freq = None
88+
self.next_freq_change_allowed_at = time.monotonic()
89+
90+
def run(self):
91+
while True:
92+
self.process_button_presses()
93+
if time.monotonic() >= self.next_freq_change_allowed_at:
94+
self.next_freq_change_allowed_at = time.monotonic() + MIN_NOTE_PLAY_SECONDS
95+
self.change_tone_if_needed()
96+
97+
@staticmethod
98+
def update_pixel(circle_pos):
99+
"""Manage the display on the NeoPixels of the current circle position"""
100+
cpx.pixels.fill((0, 0, 0))
101+
# Light the pixels clockwise from “1 o’clock” with the USB connector on the bottom
102+
pixel_index = (4 - circle_pos) % 10
103+
# Use a different color after all ten LEDs used
104+
color = (0, 255, 0) if circle_pos <= 9 else (255, 255, 0)
105+
cpx.pixels[pixel_index] = color
106+
107+
@staticmethod
108+
def tilt():
109+
"""Normalize the Y-Axis Tilt"""
110+
standard_gravity = 9.81 # Acceleration (m/s²) due to gravity at the earth’s surface
111+
constrained_accel = min(max(0.0, -cpx.acceleration[1]), standard_gravity)
112+
return constrained_accel / standard_gravity
113+
114+
def process_button_presses(self):
115+
"""For each of the buttons A and B, if pushed, advance forward or backward"""
116+
for button_index, direction in enumerate((1, -1)):
117+
if self.button.pressed(button_index):
118+
self.freq_maker.advance(direction)
119+
TiltingArpeggios.update_pixel(self.freq_maker.circle_pos)
120+
121+
def change_tone_if_needed(self):
122+
"""Find the frequency for the current arpeggio and tilt, and restart the tone if changed"""
123+
arpeggio_index = 0 if cpx.switch else 1
124+
freq = self.freq_maker.freq(TiltingArpeggios.tilt(), arpeggio_index)
125+
if freq != self.last_freq:
126+
self.last_freq = freq
127+
cpx.stop_tone()
128+
cpx.start_tone(freq)
129+
130+
131+
TiltingArpeggios().run()

0 commit comments

Comments
 (0)