Skip to content

Create Tilting Arpeggios #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 30, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions examples/advanced_examples/circuitplayground_tilting_arpeggios.py
Original file line number Diff line number Diff line change
@@ -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()