Skip to content

Commit 14db68b

Browse files
committed
Create Tilting Arpeggios
This program plays notes from arpeggios in a circle of fourths. Y-axis tilt chooses the note. Buttons A and B advance forward and backward through the circle. The switch selects the type of arpeggio, either dominant seventh or blues.
1 parent d87ea26 commit 14db68b

File tree

1 file changed

+119
-0
lines changed

1 file changed

+119
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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://youtu.be/... (TODO)
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+
25+
26+
class FrequencyMaker:
27+
"""Provide frequencies for playing notes"""
28+
def __init__(self):
29+
num_octaves_to_pre_compute = NUM_OCTAVES + 2
30+
num_freqs = HS_OCT * num_octaves_to_pre_compute
31+
32+
def calc_freq(i):
33+
return STARTING_NOTE * 2 ** (i / HS_OCT)
34+
35+
self.note_frequencies = [calc_freq(i) for i in range(num_freqs)]
36+
self.arpeg_note_indexes = FrequencyMaker.create_arpeggios(num_octaves_to_pre_compute)
37+
self.circle_pos = 0
38+
self.key_offset = 0
39+
40+
@staticmethod
41+
def create_arpeggios(num_octaves):
42+
"""Create a list of arpeggios, where each one is a list of chromatic scale note indexes"""
43+
return [FrequencyMaker.create_arpeggio(arpeggio, num_octaves) for arpeggio in ARPEGGIOS]
44+
45+
@staticmethod
46+
def create_arpeggio(arpeggio, num_octaves):
47+
return [octave * HS_OCT + note for octave in range(num_octaves) for note in arpeggio]
48+
49+
def advance(self, amount):
50+
"""Advance forward or backward through the circle of fourths"""
51+
self.circle_pos = (self.circle_pos + amount) % HS_OCT
52+
self.key_offset = self.circle_pos * HS_4TH % HS_OCT
53+
54+
def freq(self, normalized_position, selected_arpeg):
55+
"""Return the frequency for the note at the specified position in the specified arpeggio"""
56+
indexes = self.arpeg_note_indexes[selected_arpeg]
57+
arpeg_index = int(normalized_position * (len(ARPEGGIOS[selected_arpeg]) * NUM_OCTAVES + 1))
58+
note_index = self.key_offset + indexes[arpeg_index]
59+
return self.note_frequencies[note_index]
60+
61+
62+
class ButtonDetector:
63+
def __init__(self):
64+
self.next_press_allowed_at = time.monotonic()
65+
self.buttons_on = (cpx.button_a, cpx.button_b)
66+
67+
def pressed(self, index):
68+
"""
69+
Return whether the specified button was pressed, limiting the repeat rate
70+
:param index: 0 or 1 indicating Button A or Button B
71+
:return: whether the specified button was pressed, or False if it’s too early to press again
72+
"""
73+
pressed = cpx.button_b if index else cpx.button_a
74+
if pressed:
75+
now = time.monotonic()
76+
if now >= self.next_press_allowed_at:
77+
self.next_press_allowed_at = now + 0.25
78+
return True
79+
return False
80+
81+
82+
def update_pixel(circle_pos):
83+
"""Manage the display on the NeoPixels of the current circle position"""
84+
cpx.pixels.fill((0, 0, 0))
85+
# Light the pixels clockwise from “1 o’clock” with the USB connector on the bottom
86+
pixel_index = (4 - circle_pos) % 10
87+
# Use a different color after all ten LEDs used
88+
color = (0, 255, 0) if circle_pos <= 9 else (255, 255, 0)
89+
cpx.pixels[pixel_index] = color
90+
91+
92+
def tilt():
93+
"""Normalize the Y-Axis Tilt"""
94+
standard_gravity = 9.81 # Acceleration (m/s²) due to gravity at the earth’s surface
95+
constrained_accel = min(max(0.0, -cpx.acceleration[1]), standard_gravity)
96+
return constrained_accel / standard_gravity
97+
98+
99+
cpx.pixels.brightness = 0.2
100+
freq_maker = FrequencyMaker()
101+
update_pixel(freq_maker.circle_pos)
102+
button = ButtonDetector()
103+
last_freq = None
104+
next_freq_change_allowed_at = time.monotonic()
105+
106+
while True:
107+
for button_index, direction in enumerate((1, -1)):
108+
if button.pressed(button_index):
109+
freq_maker.advance(direction)
110+
update_pixel(freq_maker.circle_pos)
111+
112+
if time.monotonic() >= next_freq_change_allowed_at:
113+
next_freq_change_allowed_at = time.monotonic() + MIN_NOTE_PLAY_SECONDS
114+
arpeggio_index = 0 if cpx.switch else 1
115+
freq = freq_maker.freq(tilt(), arpeggio_index)
116+
if freq != last_freq:
117+
last_freq = freq
118+
cpx.stop_tone()
119+
cpx.start_tone(freq)

0 commit comments

Comments
 (0)