Major update to sequencer again. Added track toggle and track clear.
This commit is contained in:
parent
cc213eae2f
commit
d30e3ce4ab
1 changed files with 205 additions and 129 deletions
|
@ -12,7 +12,12 @@
|
|||
# Each track is colour-coded: track 1 is orange, track 2 blue, track 3 is pink,
|
||||
# and track 4 is green. Tracks can be selected by pressing and holding the
|
||||
# bottom left orange track select key and then tapping one of the four track
|
||||
# keys on the row above.
|
||||
# keys on the row above. The currently focussed track's track select key (on the
|
||||
# second bottom row) is highlighted in a brighter colour.
|
||||
|
||||
# A track can be toggled on or off (no notes are sent from that track, but notes
|
||||
# are not deleted) by tapping the track's track select key. The track select LED
|
||||
# for a track toggled off will not be lit.
|
||||
|
||||
# The sequencer is started and stopped by tapping the bottom right key, which is
|
||||
# red when the sequencer is stopped, and green when it is playing.
|
||||
|
@ -20,6 +25,10 @@
|
|||
# The sequencer can be cleared by holding the track selector key (orange, bottom
|
||||
# left) and then holding the start/stop key (red/green, bottom right).
|
||||
|
||||
# A single track can be cleared by holding the track selector key, the track
|
||||
# select key (on the second bottom row) for the track you want to clear, and
|
||||
# then holding the start/stop key.
|
||||
|
||||
# Tempo can be increased or decreased by holding the tempo selector key (blue,
|
||||
# second from left, on the bottom row) and then tapping blue key on the row
|
||||
# above to shift tempo down, or the pink key to shift it up. Tempo is increased/
|
||||
|
@ -33,12 +42,10 @@
|
|||
# You'll need to connect Keybow 2040 to a computer running a DAW like Ableton,
|
||||
# or other software synth, or to a hardware synth that accepts USB MIDI.
|
||||
|
||||
# Currently, all of the notes are C3 with a velocity of 127.
|
||||
|
||||
# Tracks' notes are sent on MIDI channels 1-4.
|
||||
|
||||
# Drop the keybow2040.py file into your `lib` folder on your `CIRCUITPY` drive,
|
||||
# and then save this code in the `code.py` file
|
||||
# and then save this code in the `code.py` file.
|
||||
|
||||
# NOTE! Requires the adafruit_midi CircuitPython library also!
|
||||
|
||||
|
@ -51,6 +58,9 @@ import adafruit_midi
|
|||
from adafruit_midi.note_off import NoteOff
|
||||
from adafruit_midi.note_on import NoteOn
|
||||
|
||||
|
||||
## CONSTANTS. Change these to change the look and feel of the sequencer.
|
||||
|
||||
# These are the key numbers that represent each step in a track (the top two
|
||||
# rows of four keys)
|
||||
TRACK_KEYS = [3, 7, 11, 15, 2, 6, 10, 14]
|
||||
|
@ -110,9 +120,9 @@ MAX_BPM = 200
|
|||
KEY_HOLD_TIME = 0.25
|
||||
|
||||
# LED brightness settings for the track steps.
|
||||
PLAY_BRIGHTNESS = 1.0
|
||||
ACTIVE_BRIGHTNESS = 0.2
|
||||
STEP_BRIGHTNESS = 0.05
|
||||
HIGH_BRIGHTNESS = 1.0
|
||||
MID_BRIGHTNESS = 0.2
|
||||
LOW_BRIGHTNESS = 0.05
|
||||
|
||||
# Start on middle C and a reasonably high velocity.
|
||||
DEFAULT_NOTE = 60
|
||||
|
@ -121,6 +131,7 @@ MAX_VELOCITY = 127
|
|||
MAX_NOTE = 127
|
||||
VELOCITY_STEP = 4
|
||||
|
||||
|
||||
class Sequencer(Keybow2040):
|
||||
"""
|
||||
Represents the sequencer, with a set of Track instances, which in turn have
|
||||
|
@ -148,6 +159,17 @@ class Sequencer(Keybow2040):
|
|||
track_key.index = i
|
||||
self.track_keys.append(track_key)
|
||||
|
||||
# These keys select and change the current track.
|
||||
self.track_select_keys = []
|
||||
|
||||
for i in range(len(TRACK_SELECTOR_KEYS)):
|
||||
track_select_key = self.keys[TRACK_SELECTOR_KEYS[i]]
|
||||
track_select_key.rgb = TRACK_COLOURS[i]
|
||||
self.track_select_keys.append(track_select_key)
|
||||
|
||||
self.track_select_keys_held = [False, False, False, False]
|
||||
self.track_select_active = False
|
||||
|
||||
# Holds the list of tracks, a set of Track instances.
|
||||
self.tracks = []
|
||||
|
||||
|
@ -189,19 +211,11 @@ class Sequencer(Keybow2040):
|
|||
# The start stop key.
|
||||
self.start_stop = self.keys[START_STOP]
|
||||
self.start_stop.set_led(*STOP_COLOUR)
|
||||
self.start_stop_held = False
|
||||
|
||||
# The track selector key.
|
||||
self.track_selector = self.keys[TRACK_SELECTOR]
|
||||
self.track_selector.set_led(*TRACK_SELECTOR_COLOUR)
|
||||
self.track_selector_active = False
|
||||
|
||||
# These keys select and change the current track.
|
||||
self.track_select_keys = []
|
||||
|
||||
for i in range(len(TRACK_SELECTOR_KEYS)):
|
||||
track_select_key = self.keys[TRACK_SELECTOR_KEYS[i]]
|
||||
track_select_key.rgb = TRACK_COLOURS[i]
|
||||
self.track_select_keys.append(track_select_key)
|
||||
|
||||
# Set the key hold time for all the keys. A little shorter than the
|
||||
# default for Keybow. Makes controlling the sequencer a bit more fluid.
|
||||
|
@ -213,122 +227,121 @@ class Sequencer(Keybow2040):
|
|||
for key in self.track_keys:
|
||||
@self.on_release(key)
|
||||
def step_select(key):
|
||||
if not key.held:
|
||||
step = self.tracks[self.current_track].steps[key.index]
|
||||
step.toggle()
|
||||
if not step.active:
|
||||
current_note = step.note
|
||||
self.midi_channels[track.channel].send(NoteOff(current_note, 0))
|
||||
step.note = DEFAULT_NOTE
|
||||
step.velocity = DEFAULT_VELOCITY
|
||||
else:
|
||||
self.steps_held.remove(key.index)
|
||||
self.note_down.led_off()
|
||||
self.note_up.led_off()
|
||||
self.velocity_down.led_off()
|
||||
self.velocity_up.led_off()
|
||||
if self.tracks[self.current_track].active:
|
||||
if not key.held:
|
||||
step = self.tracks[self.current_track].steps[key.index]
|
||||
step.toggle()
|
||||
if not step.active:
|
||||
current_note = step.note
|
||||
self.midi_channels[track.channel].send(NoteOff(current_note, 0))
|
||||
step.note = DEFAULT_NOTE
|
||||
step.velocity = DEFAULT_VELOCITY
|
||||
else:
|
||||
self.steps_held.remove(key.index)
|
||||
self.note_down.led_off()
|
||||
self.note_up.led_off()
|
||||
self.velocity_down.led_off()
|
||||
self.velocity_up.led_off()
|
||||
|
||||
self.update_track_select_keys(True)
|
||||
|
||||
# When step held, toggle on the note and velocity up/down keys.
|
||||
@self.on_hold(key)
|
||||
def step_change(key):
|
||||
self.steps_held.append(key.index)
|
||||
self.note_down.set_led(*NOTE_DOWN_COLOUR)
|
||||
self.note_up.set_led(*NOTE_UP_COLOUR)
|
||||
self.velocity_down.set_led(*VELOCITY_DOWN_COLOUR)
|
||||
self.velocity_up.set_led(*VELOCITY_UP_COLOUR)
|
||||
if self.tracks[self.current_track].active:
|
||||
self.steps_held.append(key.index)
|
||||
self.note_down.set_led(*NOTE_DOWN_COLOUR)
|
||||
self.note_up.set_led(*NOTE_UP_COLOUR)
|
||||
self.velocity_down.set_led(*VELOCITY_DOWN_COLOUR)
|
||||
self.velocity_up.set_led(*VELOCITY_UP_COLOUR)
|
||||
|
||||
self.update_track_select_keys(False)
|
||||
|
||||
# Attach hold function to track selector key that sets it active and
|
||||
# lights the track select keys.
|
||||
@self.on_hold(self.track_selector)
|
||||
def track_selector_hold(key):
|
||||
self.track_selector_active = True
|
||||
self.track_select_active = True
|
||||
|
||||
for k in range(len(self.track_select_keys)):
|
||||
key = self.track_select_keys[k]
|
||||
key.set_led(*TRACK_COLOURS[k])
|
||||
key.led_on()
|
||||
for track in self.tracks:
|
||||
track.update_track_select_key = True
|
||||
|
||||
# Attach release function to track selector key that sets it inactive
|
||||
# and turns track select LEDs off.
|
||||
@self.on_release(self.track_selector)
|
||||
def track_selector_release(key):
|
||||
self.track_selector_active = False
|
||||
self.track_select_active = False
|
||||
self.update_track_select_keys(True)
|
||||
|
||||
for key in self.track_select_keys:
|
||||
key.led_off()
|
||||
# Handles track select/mute, tempo down/up, note down/up.
|
||||
#
|
||||
# If the tempo selector key (second from left, blue, on the bottom row)
|
||||
# is held, pressing the tempo keys (the left two keys on the second
|
||||
# bottom row, lit blue and pink) shifts the tempo down or up by
|
||||
# 5 bpm each time it is pressed, with a lower limit of 5 BPM and upper
|
||||
# limit of 200 BPM.
|
||||
#
|
||||
# If notes are held, then the four track select keys allow the held
|
||||
# notes MIDI note number to be shifted down/up (track select keys 0
|
||||
# and 1 respectively), or MIDI velocity to be shifted down/up (track
|
||||
# select keys 2 and 3 respectively).
|
||||
#
|
||||
# If the track selector is not held, tapping this track button toggles
|
||||
#the track on/off.
|
||||
for key in self.track_select_keys:
|
||||
|
||||
# Handles track 0 select, tempo down, note down.
|
||||
# Pressing the tempo down key shifts the tempo down by
|
||||
# 5 bpm each time it is pressed, with a lower limit of 5 BPM.
|
||||
# If notes are held, then tapping this key decrements the MIDI note
|
||||
# number by one.
|
||||
@self.on_press(self.track_select_keys[0])
|
||||
def track_select_0_press(key):
|
||||
if self.track_selector_active:
|
||||
self.current_track = 0
|
||||
elif self.tempo_select_active:
|
||||
if self.bpm > 5:
|
||||
self.bpm -= 5
|
||||
elif len(self.steps_held):
|
||||
for i in self.steps_held:
|
||||
step = self.tracks[self.current_track].steps[i]
|
||||
step.last_notes.append(step.note)
|
||||
step.note_changed = True
|
||||
if step.note > 0:
|
||||
step.note -= 1
|
||||
@self.on_press(key)
|
||||
def track_select_press(key):
|
||||
index = TRACK_SELECTOR_KEYS.index(key.number)
|
||||
if self.track_select_active:
|
||||
self.current_track = index
|
||||
elif self.tempo_select_active:
|
||||
if index == 0:
|
||||
if self.bpm > 5:
|
||||
self.bpm -= 5
|
||||
elif index == 1:
|
||||
if self.bpm < 200:
|
||||
self.bpm += 5
|
||||
elif len(self.steps_held):
|
||||
for i in self.steps_held:
|
||||
step = self.tracks[self.current_track].steps[i]
|
||||
if index == 0 or index == 1:
|
||||
step.last_notes.append(step.note)
|
||||
step.note_changed = True
|
||||
if index == 0:
|
||||
if step.note > 0:
|
||||
step.note -= 1
|
||||
elif index == 1:
|
||||
if step.note < MAX_NOTE:
|
||||
step.note += 1
|
||||
elif index == 2:
|
||||
if step.velocity > 0 + VELOCITY_STEP:
|
||||
step.velocity -= VELOCITY_STEP
|
||||
elif index == 3:
|
||||
if step.velocity <= MAX_VELOCITY - VELOCITY_STEP:
|
||||
step.velocity += VELOCITY_STEP
|
||||
else:
|
||||
self.tracks[index].active = not self.tracks[index].active
|
||||
self.tracks[index].update_track_select_key = True
|
||||
|
||||
# Handles track 1 select, tempo up, note up.
|
||||
# Pressing the tempo up key shifts the tempo up by
|
||||
# 5 bpm each time it is pressed, with an upper limit of 200 BPM.
|
||||
# If notes are held, then tapping this key increments the MIDI note
|
||||
# number by one.
|
||||
@self.on_press(self.track_select_keys[1])
|
||||
def track_select_1_press(key):
|
||||
if self.track_selector_active:
|
||||
self.current_track = 1
|
||||
elif self.tempo_select_active:
|
||||
if self.bpm < 200:
|
||||
self.bpm += 5
|
||||
elif len(self.steps_held):
|
||||
for i in self.steps_held:
|
||||
step = self.tracks[self.current_track].steps[i]
|
||||
step.last_notes.append(step.note)
|
||||
step.note_changed = True
|
||||
if step.note < MAX_NOTE:
|
||||
step.note += 1
|
||||
# Handlers to hold held states of track select keys.
|
||||
for key in self.track_select_keys:
|
||||
@self.on_hold(key)
|
||||
def track_select_key_hold(key):
|
||||
index = TRACK_SELECTOR_KEYS.index(key.number)
|
||||
self.track_select_keys_held[index] = True
|
||||
|
||||
# Handles track 2 select, velocity down.
|
||||
# If notes are held, then tapping this key decrements the velocity by
|
||||
# four.
|
||||
@self.on_press(self.track_select_keys[2])
|
||||
def track_select_2_press(key):
|
||||
if self.track_selector_active:
|
||||
self.current_track = 2
|
||||
elif len(self.steps_held):
|
||||
for i in self.steps_held:
|
||||
step = self.tracks[self.current_track].steps[i]
|
||||
if step.velocity > 0 + VELOCITY_STEP:
|
||||
step.velocity -= VELOCITY_STEP
|
||||
|
||||
# Handles track 3 select, velocity up.
|
||||
# If notes are held, then tapping this key increments the velocity by
|
||||
# four.
|
||||
@self.on_press(self.track_select_keys[3])
|
||||
def track_select_3_press(key):
|
||||
if self.track_selector_active:
|
||||
self.current_track = 3
|
||||
elif len(self.steps_held):
|
||||
for i in self.steps_held:
|
||||
step = self.tracks[self.current_track].steps[i]
|
||||
if step.velocity <= MAX_VELOCITY - VELOCITY_STEP:
|
||||
step.velocity += VELOCITY_STEP
|
||||
@self.on_release(key)
|
||||
def track_select_key_release(key):
|
||||
index = TRACK_SELECTOR_KEYS.index(key.number)
|
||||
self.track_select_keys_held[index] = False
|
||||
|
||||
# Attach press function to start/stop key that toggles whether the
|
||||
# sequencer is running and toggles its colour between green (running)
|
||||
# and red (not running).
|
||||
@self.on_press(self.start_stop)
|
||||
def start_stop_toggle(key):
|
||||
if not self.track_selector_active:
|
||||
if not self.track_select_active:
|
||||
if self.running:
|
||||
self.running = False
|
||||
key.set_led(*STOP_COLOUR)
|
||||
|
@ -338,13 +351,24 @@ class Sequencer(Keybow2040):
|
|||
|
||||
# Attach hold function, so that when the track selector key is held and
|
||||
# the start/stop key is also held, clear all of the steps on all of the
|
||||
# tracks.
|
||||
# tracks. If a track select key is held, then clear just that track.
|
||||
@self.on_hold(self.start_stop)
|
||||
def start_stop_hold(key):
|
||||
if self.track_selector_active:
|
||||
self.clear_tracks()
|
||||
for track in self.tracks:
|
||||
track.midi_panic()
|
||||
self.start_stop_held = True
|
||||
|
||||
if self.track_select_active:
|
||||
if not any(self.track_select_keys_held):
|
||||
self.clear_tracks()
|
||||
for track in self.tracks:
|
||||
track.midi_panic()
|
||||
else:
|
||||
for i, state in enumerate(self.track_select_keys_held):
|
||||
if state:
|
||||
self.tracks[i].clear_steps()
|
||||
|
||||
@self.on_release(self.start_stop)
|
||||
def start_stop_release(key):
|
||||
self.start_stop_held = False
|
||||
|
||||
# Attach hold function that lights the tempo down/up keys when the
|
||||
# tempo selector key is held.
|
||||
|
@ -355,6 +379,7 @@ class Sequencer(Keybow2040):
|
|||
self.tempo_up.set_led(*TEMPO_UP_COLOUR)
|
||||
self.track_select_keys[2].led_off()
|
||||
self.track_select_keys[3].led_off()
|
||||
self.update_track_select_keys(False)
|
||||
|
||||
# Attach release function that furns off the tempo down/up LEDs.
|
||||
@self.on_release(self.tempo_selector)
|
||||
|
@ -362,6 +387,7 @@ class Sequencer(Keybow2040):
|
|||
self.tempo_select_active = False
|
||||
self.tempo_down.led_off()
|
||||
self.tempo_up.led_off()
|
||||
self.update_track_select_keys(True)
|
||||
|
||||
def update(self):
|
||||
# Update the superclass (Keybow2040).
|
||||
|
@ -410,6 +436,13 @@ class Sequencer(Keybow2040):
|
|||
if this_step.active:
|
||||
self.midi_channels[track.channel].send(NoteOn(this_note, this_vel))
|
||||
|
||||
# If track is not active, send note off for last note and this note.
|
||||
else:
|
||||
last_note = track.steps[self.last_step_num].note
|
||||
this_note = track.steps[self.this_step_num].note
|
||||
self.midi_channels[track.channel].send(NoteOff(last_note, 0))
|
||||
self.midi_channels[track.channel].send(NoteOff(this_note, 0))
|
||||
|
||||
# This step is now the last step!
|
||||
last_step = this_step
|
||||
self.last_step_num = self.this_step_num
|
||||
|
@ -434,6 +467,11 @@ class Sequencer(Keybow2040):
|
|||
for track in self.tracks:
|
||||
track.clear_steps()
|
||||
|
||||
def update_track_select_keys(self, state):
|
||||
# Updates all of the track select keys' states in one go.
|
||||
for track in self.tracks:
|
||||
track.update_track_select_key = state
|
||||
|
||||
|
||||
class Track:
|
||||
"""
|
||||
|
@ -451,6 +489,9 @@ class Track:
|
|||
self.steps = []
|
||||
self.sequencer = sequencer
|
||||
self.track_keys = self.sequencer.track_keys
|
||||
self.update_track_leds = False
|
||||
self.update_track_select_key = True
|
||||
self.select_key = self.sequencer.track_select_keys[self.index]
|
||||
|
||||
# For each key in the track, create a Step instance and add to
|
||||
# self.steps.
|
||||
|
@ -478,6 +519,29 @@ class Track:
|
|||
else:
|
||||
self.focussed = False
|
||||
|
||||
r, g, b = TRACK_COLOURS[self.index]
|
||||
|
||||
# Only update these keys if required, as it affects the BPM when
|
||||
# constantly updating them. Light the focussed track in a bright colour.
|
||||
# Turn the LED off for tracks that aren't active.
|
||||
if self.update_track_select_key:
|
||||
if not self.sequencer.track_select_active:
|
||||
if self.active:
|
||||
if not self.focussed:
|
||||
r, g, b = rgb_with_brightness(r, g, b, brightness=LOW_BRIGHTNESS)
|
||||
self.select_key.set_led(r, g, b)
|
||||
else:
|
||||
r, g, b = rgb_with_brightness(r, g, b, brightness=HIGH_BRIGHTNESS)
|
||||
self.select_key.set_led(r, g, b)
|
||||
else:
|
||||
self.select_key.led_off()
|
||||
self.update_track_select_key = False
|
||||
else:
|
||||
r, g, b = rgb_with_brightness(r, g, b, brightness=HIGH_BRIGHTNESS)
|
||||
self.select_key.set_led(r, g, b)
|
||||
self.update_track_select_key = False
|
||||
|
||||
|
||||
def update_steps(self):
|
||||
# Update a tracks steps.
|
||||
for step in self.steps:
|
||||
|
@ -489,6 +553,7 @@ class Track:
|
|||
step.active = False
|
||||
|
||||
def midi_panic(self):
|
||||
# Send note off messages for every note on this track's channel.
|
||||
for i in range(128):
|
||||
self.sequencer.midi_channels[self.channel].send(NoteOff(i, 0))
|
||||
|
||||
|
@ -537,24 +602,27 @@ class Step:
|
|||
if self.track.focussed:
|
||||
# Only update the LEDs when the sequencer is running.
|
||||
if self.sequencer.running:
|
||||
# Make an active step that is currently being played full
|
||||
# brightness.
|
||||
if self.playing and self.active:
|
||||
self.set_led(r, g, b, PLAY_BRIGHTNESS)
|
||||
if self.track.active:
|
||||
# Make an active step that is currently being played full
|
||||
# brightness.
|
||||
if self.playing and self.active:
|
||||
self.set_led(r, g, b, HIGH_BRIGHTNESS)
|
||||
|
||||
# Make an inactive step that is "playing" (the current step)
|
||||
# the dimmest brightness, but bright enough to indicate the
|
||||
# step the sequencer is on.
|
||||
if self.playing and not self.active:
|
||||
self.set_led(r, g, b, STEP_BRIGHTNESS)
|
||||
# Make an inactive step that is "playing" (the current step)
|
||||
# the dimmest brightness, but bright enough to indicate the
|
||||
# step the sequencer is on.
|
||||
if self.playing and not self.active:
|
||||
self.set_led(r, g, b, LOW_BRIGHTNESS)
|
||||
|
||||
# Make an active step that is not playing a low-medium
|
||||
# brightness to indicate that it is toggled active.
|
||||
if not self.playing and self.active:
|
||||
self.set_led(r, g, b, ACTIVE_BRIGHTNESS)
|
||||
# Make an active step that is not playing a low-medium
|
||||
# brightness to indicate that it is toggled active.
|
||||
if not self.playing and self.active:
|
||||
self.set_led(r, g, b, MID_BRIGHTNESS)
|
||||
|
||||
# Turn not playing, not active steps off.
|
||||
if not self.playing and not self.active:
|
||||
# Turn not playing, not active steps off.
|
||||
if not self.playing and not self.active:
|
||||
self.set_led(0, 0, 0, 0)
|
||||
else:
|
||||
self.set_led(0, 0, 0, 0)
|
||||
|
||||
# If the sequencer is not running, still show the active steps.
|
||||
|
@ -564,10 +632,18 @@ class Step:
|
|||
else:
|
||||
self.set_led(0, 0, 0, 0)
|
||||
|
||||
# Set up Keybow
|
||||
|
||||
def rgb_with_brightness(r, g, b, brightness=1.0):
|
||||
# Allows an RGB value to be altered with a brightness
|
||||
# value from 0.0 to 1.0.
|
||||
r, g, b = (int(c * brightness) for c in (r, g, b))
|
||||
return r, g, b
|
||||
|
||||
|
||||
# Set up Keybow's I2C bus.
|
||||
i2c = board.I2C()
|
||||
|
||||
# Instatiate the sequencer.
|
||||
# Instantiate the sequencer.
|
||||
sequencer = Sequencer(i2c)
|
||||
|
||||
while True:
|
||||
|
|
Loading…
Reference in a new issue