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