Skip to content

Commit 2008ba5

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 2008ba5

File tree

1 file changed

+116
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)