From b6a8e5f915906c414039a478570fef44e4365ef7 Mon Sep 17 00:00:00 2001 From: Dave Briccetti Date: Mon, 23 Sep 2019 10:17:11 -0700 Subject: [PATCH] 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. --- .../circuitplayground_tilting_arpeggios.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 examples/advanced_examples/circuitplayground_tilting_arpeggios.py diff --git a/examples/advanced_examples/circuitplayground_tilting_arpeggios.py b/examples/advanced_examples/circuitplayground_tilting_arpeggios.py new file mode 100644 index 0000000..9915046 --- /dev/null +++ b/examples/advanced_examples/circuitplayground_tilting_arpeggios.py @@ -0,0 +1,131 @@ +"""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. + +You can ignore the FrequencyProvider class if you’re just interested in the CPX interface. + +See a code walkthrough here: https://www.youtube.com/watch?v=cDhqyT3ZN0g +""" + +# pylint: disable=R0903 +import time +from adafruit_circuitplayground.express import cpx + +HS_OCT = 12 # Half-steps per octave +HS_4TH = 5 # Half-steps in a fourth +ARPEGGIOS = ( + (0, 4, 7, 10), # Dominant seventh + (0, 3, 5, 6, 7, 10)) # Blues +NUM_OCTAVES = 2 +STARTING_NOTE = 233.08 +MIN_NOTE_PLAY_SECONDS = 0.25 +BUTTON_REPEAT_AFTER_SECONDS = 0.25 + + +class FrequencyMaker: + """Provide frequencies for playing notes""" + def __init__(self): + num_octaves_to_pre_compute = NUM_OCTAVES + 2 + num_freqs = HS_OCT * num_octaves_to_pre_compute + + def calc_freq(i): + return STARTING_NOTE * 2 ** (i / HS_OCT) + + self.note_frequencies = [calc_freq(i) for i in range(num_freqs)] + self.arpeg_note_indexes = FrequencyMaker.create_arpeggios(num_octaves_to_pre_compute) + self.circle_pos = 0 + self.key_offset = 0 + + @staticmethod + def create_arpeggios(num_octaves): + """Create a list of arpeggios, where each one is a list of chromatic scale note indexes""" + return [FrequencyMaker.create_arpeggio(arpeggio, num_octaves) for arpeggio in ARPEGGIOS] + + @staticmethod + def create_arpeggio(arpeggio, num_octaves): + return [octave * HS_OCT + note for octave in range(num_octaves) for note in arpeggio] + + def advance(self, amount): + """Advance forward or backward through the circle of fourths""" + self.circle_pos = (self.circle_pos + amount) % HS_OCT + self.key_offset = self.circle_pos * HS_4TH % HS_OCT + + def freq(self, normalized_position, selected_arpeg): + """Return the frequency for the note at the specified position in the specified arpeggio""" + selected_arpeg_note_indexes = self.arpeg_note_indexes[selected_arpeg] + num_notes_in_selected_arpeg = len(ARPEGGIOS[selected_arpeg]) + num_arpeg_notes_in_range = num_notes_in_selected_arpeg * NUM_OCTAVES + 1 + arpeg_index = int(normalized_position * num_arpeg_notes_in_range) + note_index = self.key_offset + selected_arpeg_note_indexes[arpeg_index] + return self.note_frequencies[note_index] + + +class ButtonDetector: + def __init__(self): + self.next_press_allowed_at = time.monotonic() + self.buttons_on = (cpx.button_a, cpx.button_b) + + def pressed(self, index): + """Return whether the specified button (0=A, 1=B) was pressed, limiting the repeat rate""" + pressed = cpx.button_b if index else cpx.button_a + if pressed: + now = time.monotonic() + if now >= self.next_press_allowed_at: + self.next_press_allowed_at = now + BUTTON_REPEAT_AFTER_SECONDS + return True + return False + + +class TiltingArpeggios: + def __init__(self): + cpx.pixels.brightness = 0.2 + self.freq_maker = FrequencyMaker() + TiltingArpeggios.update_pixel(self.freq_maker.circle_pos) + self.button = ButtonDetector() + self.last_freq = None + self.next_freq_change_allowed_at = time.monotonic() + + def run(self): + while True: + self.process_button_presses() + if time.monotonic() >= self.next_freq_change_allowed_at: + self.next_freq_change_allowed_at = time.monotonic() + MIN_NOTE_PLAY_SECONDS + self.change_tone_if_needed() + + @staticmethod + def update_pixel(circle_pos): + """Manage the display on the NeoPixels of the current circle position""" + cpx.pixels.fill((0, 0, 0)) + # Light the pixels clockwise from “1 o’clock” with the USB connector on the bottom + pixel_index = (4 - circle_pos) % 10 + # Use a different color after all ten LEDs used + color = (0, 255, 0) if circle_pos <= 9 else (255, 255, 0) + cpx.pixels[pixel_index] = color + + @staticmethod + def tilt(): + """Normalize the Y-Axis Tilt""" + standard_gravity = 9.81 # Acceleration (m/s²) due to gravity at the earth’s surface + constrained_accel = min(max(0.0, -cpx.acceleration[1]), standard_gravity) + return constrained_accel / standard_gravity + + def process_button_presses(self): + """For each of the buttons A and B, if pushed, advance forward or backward""" + for button_index, direction in enumerate((1, -1)): + if self.button.pressed(button_index): + self.freq_maker.advance(direction) + TiltingArpeggios.update_pixel(self.freq_maker.circle_pos) + + def change_tone_if_needed(self): + """Find the frequency for the current arpeggio and tilt, and restart the tone if changed""" + arpeggio_index = 0 if cpx.switch else 1 + freq = self.freq_maker.freq(TiltingArpeggios.tilt(), arpeggio_index) + if freq != self.last_freq: + self.last_freq = freq + cpx.stop_tone() + cpx.start_tone(freq) + + +TiltingArpeggios().run()