Updating MIDI sequencer with note and velocity control.

This commit is contained in:
sandyjmacdonald 2021-03-16 20:59:10 +00:00
parent ec06dfa59c
commit 56615a04c6
2 changed files with 172 additions and 57 deletions

View file

@ -34,11 +34,11 @@ midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=5)
rgb = (255, 50, 0) rgb = (255, 50, 0)
# MIDI velocity. # MIDI velocity.
start_note = 60 start_note = 36
velocity = 100 velocity = 100
# Beats per minute # Beats per minute
bpm = 120 bpm = 80
# Play 16th notes # Play 16th notes
note_length = 1/16 note_length = 1/16
@ -50,7 +50,7 @@ note_time = 60 / bpm * (note_length * 4)
# 0 = up # 0 = up
# 1 = down # 1 = down
# 2 = up-down # 2 = up-down
arp_style = 0 arp_style = 2
# Start the arp in a forwards direction (1) if the style is up or up-down, or # Start the arp in a forwards direction (1) if the style is up or up-down, or
# or backwards (-1) if the style is down. # or backwards (-1) if the style is down.

View file

@ -9,7 +9,7 @@
# currently playing step in the sequence is shown with a moving LED across the # currently playing step in the sequence is shown with a moving LED across the
# eight steps. # eight steps.
# Each track is colour-coded: track 1 is orange, track 2 teal, track 3 is pink, # 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 # 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 # bottom left orange track select key and then tapping one of the four track
# keys on the row above. # keys on the row above.
@ -20,11 +20,16 @@
# The sequencer can be cleared by holding the track selector key (orange, bottom # 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). # left) and then holding the start/stop key (red/green, bottom right).
# Tempo can be increased or decreased by holding the tempo selector key (teal, # Tempo can be increased or decreased by holding the tempo selector key (blue,
# second from left, on the bottom row) and then tapping teal key on the row # 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/ # above to shift tempo down, or the pink key to shift it up. Tempo is increased/
# decreased by 5 BPM on each press. # decreased by 5 BPM on each press.
# If an active step is held down, the second bottom row of keys lights to allow
# the note to be shifted down/up (the left two keys, decremented/incremented by
# one each time) and the note velocity to be shifted down/up (the right two keys
# decremented/incremented by four each time).
# You'll need to connect Keybow 2040 to a computer running a DAW like Ableton, # 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. # or other software synth, or to a hardware synth that accepts USB MIDI.
@ -50,13 +55,14 @@ from adafruit_midi.note_on import NoteOn
# rows of four keys) # rows of four keys)
TRACK_KEYS = [3, 7, 11, 15, 2, 6, 10, 14] TRACK_KEYS = [3, 7, 11, 15, 2, 6, 10, 14]
# The colours for the LEDs on each track: orange, teal, pink, green ORANGE = (255, 255, 0)
TRACK_COLOURS = [ BLUE = (0, 255, 175)
(255, 255, 0), PINK = (255, 0, 255)
(0, 255, 175), GREEN = (0, 255, 0)
(255, 0, 255), RED = (255, 0, 0)
(0, 255, 0)
] # The colours for the LEDs on each track: orange, blue, pink, green
TRACK_COLOURS = [ORANGE, BLUE, PINK, GREEN]
# The MIDI channels for each track in turn: 1, 2, 3, 4 # The MIDI channels for each track in turn: 1, 2, 3, 4
MIDI_CHANNELS = [0, 1, 2, 3] MIDI_CHANNELS = [0, 1, 2, 3]
@ -65,26 +71,40 @@ MIDI_CHANNELS = [0, 1, 2, 3]
# keys, the four keys on the row above it. # keys, the four keys on the row above it.
TRACK_SELECTOR = 0 TRACK_SELECTOR = 0
TRACK_SELECTOR_KEYS = [1, 5, 9, 13] TRACK_SELECTOR_KEYS = [1, 5, 9, 13]
TRACK_SELECTOR_COLOUR = (255, 255, 0) TRACK_SELECTOR_COLOUR = ORANGE
# The bottom right key. When pressed, it toggles the sequencer on or off. Green # The bottom right key. When pressed, it toggles the sequencer on or off. Green
# indicates that it is currently playing, red that it is stopped. # indicates that it is currently playing, red that it is stopped.
START_STOP = 12 START_STOP = 12
START_COLOUR = (0, 255, 0) START_COLOUR = GREEN
STOP_COLOUR = (255, 0, 0) STOP_COLOUR = RED
# The key second from left on the bottom row, teal. When pressed, it brings up # The key second from left on the bottom row, blue. When pressed, it brings up
# the tempo down/up buttons on the row above it. The left teal key shifts the # the tempo down/up buttons on the row above it. The left blue key shifts the
# tempo down, the right pink key shifts the tempo up. # tempo down, the right pink key shifts the tempo up.
TEMPO_SELECTOR = 4 TEMPO_SELECTOR = 4
TEMPO_SELECTOR_COLOUR = (0, 255, 175) TEMPO_SELECTOR_COLOUR = BLUE
TEMPO_DOWN = 5 TEMPO_DOWN = 1
TEMPO_DOWN_COLOUR = (0, 255, 175) TEMPO_DOWN_COLOUR = BLUE
TEMPO_UP = 9 TEMPO_UP = 5
TEMPO_UP_COLOUR = (255, 0, 255) TEMPO_UP_COLOUR = PINK
NOTE_DOWN = 1
NOTE_DOWN_COLOUR = BLUE
NOTE_UP = 5
NOTE_UP_COLOUR = PINK
# When an active step is held down, the second bottom row of keys lights to
# allow the note to be shifted down/up (the left two keys) and the note velocity
# to be shifted down/up (the right two keys).
VELOCITY_DOWN = 9
VELOCITY_DOWN_COLOUR = BLUE
VELOCITY_UP = 13
VELOCITY_UP_COLOUR = PINK
# The default starting BPM. # The default starting BPM.
BPM = 85 BPM = 85
MAX_BPM = 200
# Dictates the time after which a key is "held". # Dictates the time after which a key is "held".
KEY_HOLD_TIME = 0.25 KEY_HOLD_TIME = 0.25
@ -94,6 +114,13 @@ PLAY_BRIGHTNESS = 1.0
ACTIVE_BRIGHTNESS = 0.2 ACTIVE_BRIGHTNESS = 0.2
STEP_BRIGHTNESS = 0.05 STEP_BRIGHTNESS = 0.05
# Start on middle C and a reasonably high velocity.
DEFAULT_NOTE = 60
DEFAULT_VELOCITY = 99
MAX_VELOCITY = 127
MAX_NOTE = 127
VELOCITY_STEP = 4
class Sequencer(Keybow2040): class Sequencer(Keybow2040):
""" """
Represents the sequencer, with a set of Track instances, which in turn have Represents the sequencer, with a set of Track instances, which in turn have
@ -113,6 +140,14 @@ class Sequencer(Keybow2040):
midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=channel) midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=channel)
self.midi_channels.append(midi) self.midi_channels.append(midi)
# These keys represent the steps on the tracks.
self.track_keys = []
for i in range(len(TRACK_KEYS)):
track_key = self.keys[TRACK_KEYS[i]]
track_key.index = i
self.track_keys.append(track_key)
# Holds the list of tracks, a set of Track instances. # Holds the list of tracks, a set of Track instances.
self.tracks = [] self.tracks = []
@ -133,6 +168,13 @@ class Sequencer(Keybow2040):
self.num_steps = 8 self.num_steps = 8
self.this_step_num = 0 self.this_step_num = 0
self.last_step_num = 0 self.last_step_num = 0
self.steps_held = []
# Note change attributes.
self.note_down = self.keys[NOTE_DOWN]
self.note_up = self.keys[NOTE_UP]
self.velocity_down = self.keys[VELOCITY_DOWN]
self.velocity_up = self.keys[VELOCITY_UP]
# Is the sequencer running? # Is the sequencer running?
self.running = False self.running = False
@ -157,9 +199,9 @@ class Sequencer(Keybow2040):
self.track_select_keys = [] self.track_select_keys = []
for i in range(len(TRACK_SELECTOR_KEYS)): for i in range(len(TRACK_SELECTOR_KEYS)):
track_key = self.keys[TRACK_SELECTOR_KEYS[i]] track_select_key = self.keys[TRACK_SELECTOR_KEYS[i]]
track_key.rgb = TRACK_COLOURS[i] track_select_key.rgb = TRACK_COLOURS[i]
self.track_select_keys.append(track_key) self.track_select_keys.append(track_select_key)
# Set the key hold time for all the keys. A little shorter than the # 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. # default for Keybow. Makes controlling the sequencer a bit more fluid.
@ -168,14 +210,32 @@ class Sequencer(Keybow2040):
# Attach step_select function to keys in track steps. If pressed it # Attach step_select function to keys in track steps. If pressed it
# toggles the state of the step. # toggles the state of the step.
for i in range(len(TRACK_KEYS)): for key in self.track_keys:
key = self.keys[TRACK_KEYS[i]] @self.on_release(key)
@self.on_press(key)
def step_select(key): def step_select(key):
step_num = TRACK_KEYS.index(key.number) if not key.held:
step = self.tracks[self.current_track].steps[step_num] step = self.tracks[self.current_track].steps[key.index]
step.toggle() 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()
# 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)
# Attach hold function to track selector key that sets it active and # Attach hold function to track selector key that sets it active and
# lights the track select keys. # lights the track select keys.
@ -183,7 +243,9 @@ class Sequencer(Keybow2040):
def track_selector_hold(key): def track_selector_hold(key):
self.track_selector_active = True self.track_selector_active = True
for key in self.track_select_keys: for k in range(len(self.track_select_keys)):
key = self.track_select_keys[k]
key.set_led(*TRACK_COLOURS[k])
key.led_on() key.led_on()
# Attach release function to track selector key that sets it inactive # Attach release function to track selector key that sets it inactive
@ -195,41 +257,71 @@ class Sequencer(Keybow2040):
for key in self.track_select_keys: for key in self.track_select_keys:
key.led_off() key.led_off()
# Track 0 select. # 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]) @self.on_press(self.track_select_keys[0])
def track_select_0_press(key): def track_select_0_press(key):
if self.track_selector_active: if self.track_selector_active:
self.current_track = 0 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
# Special case to handle track 1 select and tempo down. # Handles track 1 select, tempo up, note up.
# Pressing the tempo down key shifts the tempo down by # Pressing the tempo up key shifts the tempo up by
# 5 bpm each time it is pressed, with a lower limit of 5 BPM. # 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]) @self.on_press(self.track_select_keys[1])
def track_select_1_press(key): def track_select_1_press(key):
if self.track_selector_active: if self.track_selector_active:
self.current_track = 1 self.current_track = 1
else: elif self.tempo_select_active:
if self.tempo_select_active: if self.bpm < 200:
if self.bpm > 5: 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 < MAX_NOTE:
step.note += 1
# Special case to handle track 2 select and tempo up. # Handles track 2 select, velocity down.
# Pressing the tempo up key shifts the tempo up by # If notes are held, then tapping this key decrements the velocity by
# 5 bpm each time it is pressed, with an upper limit of 200 BPM. # four.
@self.on_press(self.track_select_keys[2]) @self.on_press(self.track_select_keys[2])
def track_select_2_press(key): def track_select_2_press(key):
if self.track_selector_active: if self.track_selector_active:
self.current_track = 2 self.current_track = 2
else: elif len(self.steps_held):
if self.tempo_select_active: for i in self.steps_held:
if self.bpm < 200: step = self.tracks[self.current_track].steps[i]
self.bpm += 5 if step.velocity > 0 + VELOCITY_STEP:
step.velocity -= VELOCITY_STEP
# Track 3 select. # 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]) @self.on_press(self.track_select_keys[3])
def track_select_3_press(key): def track_select_3_press(key):
if self.track_selector_active: if self.track_selector_active:
self.current_track = 3 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
# Attach press function to start/stop key that toggles whether the # Attach press function to start/stop key that toggles whether the
# sequencer is running and toggles its colour between green (running) # sequencer is running and toggles its colour between green (running)
@ -259,6 +351,8 @@ class Sequencer(Keybow2040):
self.tempo_select_active = True self.tempo_select_active = True
self.tempo_down.set_led(*TEMPO_DOWN_COLOUR) self.tempo_down.set_led(*TEMPO_DOWN_COLOUR)
self.tempo_up.set_led(*TEMPO_UP_COLOUR) self.tempo_up.set_led(*TEMPO_UP_COLOUR)
self.track_select_keys[2].led_off()
self.track_select_keys[3].led_off()
# Attach release function that furns off the tempo down/up LEDs. # Attach release function that furns off the tempo down/up LEDs.
@self.on_release(self.tempo_selector) @self.on_release(self.tempo_selector)
@ -285,6 +379,13 @@ class Sequencer(Keybow2040):
last_step.update() last_step.update()
last_note = last_step.note last_note = last_step.note
# Helps prevent stuck notes.
if last_step.note_changed:
for note in last_step.last_notes:
self.midi_channels[track.channel].send(NoteOff(note, 0))
last_step.note_changed = False
last_step.last_notes = []
# If last step is active, send MIDI note off message. # If last step is active, send MIDI note off message.
if last_step.active: if last_step.active:
self.midi_channels[track.channel].send(NoteOff(last_note, 0)) self.midi_channels[track.channel].send(NoteOff(last_note, 0))
@ -296,6 +397,13 @@ class Sequencer(Keybow2040):
this_note = this_step.note this_note = this_step.note
this_vel = this_step.velocity this_vel = this_step.velocity
# Helps prevent stuck notes
if this_step.note_changed:
for note in this_step.last_notes:
self.midi_channels[track.channel].send(NoteOff(note, 0))
this_step.note_changed = False
this_step.last_notes = []
# If this step is active, send MIDI note on message. # If this step is active, send MIDI note on message.
if this_step.active: if this_step.active:
self.midi_channels[track.channel].send(NoteOn(this_note, this_vel)) self.midi_channels[track.channel].send(NoteOn(this_note, this_vel))
@ -319,12 +427,12 @@ class Sequencer(Keybow2040):
# Update the step_time, in case the BPM has been changed. # Update the step_time, in case the BPM has been changed.
self.step_time = 60.0 / self.bpm / (self.num_steps / 2) self.step_time = 60.0 / self.bpm / (self.num_steps / 2)
def clear_tracks(self): def clear_tracks(self):
# Clears the steps on all tracks. # Clears the steps on all tracks.
for track in self.tracks: for track in self.tracks:
track.clear_steps() track.clear_steps()
class Track: class Track:
""" """
Represents a track on the sequencer. Represents a track on the sequencer.
@ -340,13 +448,12 @@ class Track:
self.channel = channel self.channel = channel
self.steps = [] self.steps = []
self.sequencer = sequencer self.sequencer = sequencer
self.track_keys = self.sequencer.track_keys
# For each key in the track, create a Step instance and add to # For each key in the track, create a Step instance and add to
# self.steps. # self.steps.
for i in range(len(TRACK_KEYS)): for i, key in enumerate(self.track_keys):
index = i step = Step(i, key, self)
key = sequencer.keys[TRACK_KEYS[i]]
step = Step(index, key, self)
self.steps.append(step) self.steps.append(step)
# Default to having the track active. # Default to having the track active.
@ -379,6 +486,11 @@ class Track:
for step in self.steps: for step in self.steps:
step.active = False step.active = False
def midi_panic(self):
for i in range(128):
self.sequencer.midi_channels[self.channel].send(NoteOff(i, 0))
class Step: class Step:
""" """
Represents a step on a track. Represents a step on a track.
@ -393,8 +505,11 @@ class Step:
self.track = track self.track = track
self.active = False self.active = False
self.playing = False self.playing = False
self.velocity = 127 self.held = False
self.note = 60 self.velocity = DEFAULT_VELOCITY
self.note = DEFAULT_NOTE
self.last_notes = []
self.note_changed = False
self.rgb = self.track.rgb self.rgb = self.track.rgb
self.sequencer = self.track.sequencer self.sequencer = self.track.sequencer