239 lines
7.9 KiB
Python
239 lines
7.9 KiB
Python
# SPDX-FileCopyrightText: 2021 John Park for Adafruit Industries
|
|
# SPDX-License-Identifier: MIT
|
|
# Ableton Live Macropad Launcher
|
|
# In Ableton, choose "Launchpad Mini Mk3" as controller with MacroPad 2040 as in and out
|
|
# Use empty fifth scene to allow "unlaunching" of tracks with encoder modifier
|
|
import board
|
|
from adafruit_macropad import MacroPad
|
|
import displayio
|
|
import terminalio
|
|
from adafruit_simplemath import constrain
|
|
from adafruit_display_text import label
|
|
import usb_midi
|
|
import adafruit_midi
|
|
from adafruit_midi.control_change import ControlChange
|
|
from adafruit_midi.note_off import NoteOff
|
|
from adafruit_midi.note_on import NoteOn
|
|
from adafruit_midi.midi_message import MIDIUnknownEvent
|
|
|
|
macropad = MacroPad()
|
|
|
|
TITLE_TEXT = "Live Launcher 2040"
|
|
print(TITLE_TEXT)
|
|
TRACK_NAMES = ["DRUM", "BASS", "SYNTH"] # Customize these
|
|
LIVE_CC_NUMBER = 74 # CC number to send w encoder
|
|
FADER_TEXT = "cutoff" # change for intended CC name
|
|
|
|
# --- MIDI recieve is complex, so not using macropad.midi
|
|
midi = adafruit_midi.MIDI(
|
|
midi_in=usb_midi.ports[0],
|
|
in_channel=(0, 1, 2),
|
|
midi_out=usb_midi.ports[1],
|
|
out_channel=0
|
|
)
|
|
|
|
|
|
# ---Official Launchpad colors---
|
|
LP_COLORS = (
|
|
0x000000, 0x101010, 0x202020, 0x3f3f3f, 0x3f0f0f, 0x3f0000, 0x200000, 0x100000,
|
|
0x3f2e1a, 0x3f0f00, 0x200800, 0x100400, 0x3f2b0b, 0x3f3f00, 0x202000, 0x101000,
|
|
0x213f0c, 0x143f00, 0x0a2000, 0x051000, 0x123f12, 0x003f00, 0x002000, 0x001000,
|
|
0x123f17, 0x003f06, 0x002003, 0x001001, 0x123f16, 0x003f15, 0x00200b, 0x001006,
|
|
0x123f2d, 0x003f25, 0x002012, 0x001009, 0x12303f, 0x00293f, 0x001520, 0x000b10,
|
|
0x12213f, 0x00153f, 0x000b20, 0x000610, 0x0b093f, 0x00003f, 0x000020, 0x000010,
|
|
0x1a0d3e, 0x0b003f, 0x060020, 0x030010, 0x3f0f3f, 0x3f003f, 0x200020, 0x100010,
|
|
0x3f101b, 0x3f0014, 0x20000a, 0x100005, 0x3f0300, 0x250d00, 0x1d1400, 0x080d01,
|
|
0x000e00, 0x001206, 0x00051b, 0x00003f, 0x001113, 0x040032, 0x1f1f1f, 0x070707,
|
|
0x3f0000, 0x2e3f0b, 0x2b3a01, 0x183f02, 0x032200, 0x003f17, 0x00293f, 0x000a3f,
|
|
0x06003f, 0x16003f, 0x2b061e, 0x0a0400, 0x3f0c00, 0x213701, 0x1c3f05, 0x003f00,
|
|
0x0e3f09, 0x153f1b, 0x0d3f32, 0x16223f, 0x0c1430, 0x1a1439, 0x34073f, 0x3f0016,
|
|
0x3f1100, 0x2d2900, 0x233f00, 0x201601, 0x0e0a00, 0x001203, 0x031308, 0x05050a,
|
|
0x050716, 0x190e06, 0x200000, 0x36100a, 0x351204, 0x3f2f09, 0x27370b, 0x192c03,
|
|
0x05050b, 0x36341a, 0x1f3a22, 0x26253f, 0x23193f, 0x0f0f0f, 0x1c1c1c, 0x373f3f,
|
|
0x270000, 0x0d0000, 0x063300, 0x011000, 0x2d2b00, 0x0f0c00, 0x2c1400, 0x120500,
|
|
)
|
|
|
|
LP_PADS = {
|
|
81: 0, 82: 1, 83: 2,
|
|
71: 3, 72: 4, 73: 5,
|
|
61: 6, 62: 7, 63: 8,
|
|
51: 9, 52: 10, 53: 11
|
|
}
|
|
|
|
LIVE_NOTES = [81, 82, 83, 71, 72, 73, 61, 62, 63, 51, 52, 53]
|
|
CC_OFFSET = 20
|
|
modifier = False # use to add encoder switch modifier to keys for clip mute
|
|
MODIFIER_NOTES = [41, 42, 43, 41, 42, 43, 41, 42, 43, 41, 42, 43] # blank row in Live
|
|
|
|
last_position = 0 # encoder position state
|
|
|
|
# ---NeoPixel setup---
|
|
BRIGHT = 0.125
|
|
DIM = 0.0625
|
|
macropad.pixels.brightness = BRIGHT
|
|
|
|
# ---Display setup---
|
|
display = board.DISPLAY
|
|
screen = displayio.Group(max_size=12)
|
|
display.show(screen)
|
|
WIDTH = 128
|
|
HEIGHT = 64
|
|
FONT = terminalio.FONT
|
|
# Draw a title label
|
|
title = TITLE_TEXT
|
|
title_area = label.Label(FONT, text=title, color=0xFFFFFF, x=6, y=3)
|
|
screen.append(title_area)
|
|
|
|
# --- create display strings and positions
|
|
x1 = 5
|
|
x2 = 35
|
|
x3 = 65
|
|
y1 = 17
|
|
y2 = 27
|
|
y3 = 37
|
|
y4 = 47
|
|
y5 = 57
|
|
|
|
# ---Push knob text setup
|
|
push_text_area = label.Label(FONT, text="[o]", color=0xffffff, x=WIDTH-22, y=y2)
|
|
screen.append(push_text_area)
|
|
|
|
# ---CC knob text setup
|
|
fader_text_area = label.Label(FONT, text=FADER_TEXT, color=0xffffff, x=WIDTH - 42, y=y4)
|
|
screen.append(fader_text_area)
|
|
# --- cc value display
|
|
cc_val_text = str(CC_OFFSET)
|
|
cc_val_text_area = label.Label(FONT, text=cc_val_text, color=0xffffff, x=WIDTH - 20, y=y5)
|
|
screen.append(cc_val_text_area)
|
|
|
|
label_data = (
|
|
# text, x, y
|
|
(TRACK_NAMES[0], x1, y1), (TRACK_NAMES[1], x2, y1), (TRACK_NAMES[2], x3, y1),
|
|
(".", x1, y2), (".", x2, y2), (".", x3, y2),
|
|
(".", x1, y3), (".", x2, y3), (".", x3, y3),
|
|
(".", x1, y4), (".", x2, y4), (".", x3, y4),
|
|
(".", x1, y5), (".", x2, y5), (".", x3, y5)
|
|
)
|
|
|
|
labels = []
|
|
|
|
for data in label_data:
|
|
text, x, y = data
|
|
label_area = label.Label(FONT, text=text, color=0xffffff)
|
|
group = displayio.Group(max_size=4, x=x, y=y)
|
|
group.append(label_area)
|
|
screen.append(group)
|
|
labels.append(label_area) # these are individually addressed later
|
|
|
|
num = 1
|
|
|
|
while True:
|
|
msg_in = midi.receive()
|
|
if isinstance(msg_in, NoteOn) and msg_in.velocity != 0:
|
|
print(
|
|
"received NoteOn",
|
|
"from channel",
|
|
msg_in.channel + 1,
|
|
"MIDI note",
|
|
msg_in.note,
|
|
"velocity",
|
|
msg_in.velocity,
|
|
"\n"
|
|
)
|
|
# send neopixel lightup code to key, text to display
|
|
if msg_in.note in LP_PADS:
|
|
macropad.pixels[LP_PADS[msg_in.note]] = LP_COLORS[msg_in.velocity]
|
|
macropad.pixels.show()
|
|
if msg_in.velocity == 21: # active pad is indicated by Live as vel 21
|
|
labels[LP_PADS[msg_in.note]+3].text = "o"
|
|
else:
|
|
labels[LP_PADS[msg_in.note]+3].text = "."
|
|
|
|
elif isinstance(msg_in, NoteOff):
|
|
print(
|
|
"received NoteOff",
|
|
"from channel",
|
|
msg_in.channel + 1,
|
|
"\n"
|
|
)
|
|
|
|
elif isinstance(msg_in, NoteOn) and msg_in.velocity == 0:
|
|
print(
|
|
"received NoteOff",
|
|
"from channel",
|
|
msg_in.channel + 1,
|
|
"MIDI note",
|
|
msg_in.note,
|
|
"velocity",
|
|
msg_in.velocity,
|
|
"\n"
|
|
)
|
|
|
|
elif isinstance(msg_in, ControlChange):
|
|
print(
|
|
"received CC",
|
|
"from channel",
|
|
msg_in.channel + 1,
|
|
"controller",
|
|
msg_in.control,
|
|
"value",
|
|
msg_in.value,
|
|
"\n"
|
|
)
|
|
|
|
elif isinstance(msg_in, MIDIUnknownEvent):
|
|
# Message are only known if they are imported
|
|
print("Unknown MIDI event status ", msg_in.status)
|
|
|
|
elif msg_in is not None:
|
|
midi.send(msg_in)
|
|
|
|
key_event = macropad.keys.events.get() # check for keypad events
|
|
|
|
if not key_event: # Event is None; no keypad event happened, do other stuff
|
|
|
|
position = macropad.encoder # store encoder position state
|
|
cc_position = constrain((position + CC_OFFSET), 0, 127) # lock to cc range
|
|
if last_position is None or position != last_position:
|
|
|
|
if position < last_position:
|
|
midi.send(ControlChange(LIVE_CC_NUMBER, cc_position))
|
|
print("CC", cc_position)
|
|
cc_val_text_area.text = str(cc_position)
|
|
|
|
elif position > last_position:
|
|
midi.send(ControlChange(LIVE_CC_NUMBER, cc_position))
|
|
print("CC", cc_position)
|
|
cc_val_text_area.text = str(cc_position)
|
|
last_position = position
|
|
|
|
macropad.encoder_switch_debounced.update() # check the encoder switch w debouncer
|
|
if macropad.encoder_switch_debounced.pressed:
|
|
print("Mod")
|
|
push_text_area.text = "[.]"
|
|
modifier = True
|
|
macropad.pixels.brightness = DIM
|
|
|
|
if macropad.encoder_switch_debounced.released:
|
|
modifier = False
|
|
push_text_area.text = "[o]"
|
|
macropad.pixels.brightness = BRIGHT
|
|
|
|
continue
|
|
|
|
num = key_event.key_number
|
|
|
|
if key_event.pressed and not modifier:
|
|
midi.send(NoteOn(LIVE_NOTES[num], 127))
|
|
print("\nsent note", LIVE_NOTES[num], "\n")
|
|
|
|
if key_event.pressed and modifier:
|
|
midi.send(NoteOn(MODIFIER_NOTES[num], 127))
|
|
|
|
if key_event.released and not modifier:
|
|
midi.send(NoteOff(LIVE_NOTES[num], 0))
|
|
|
|
if key_event.released and modifier:
|
|
midi.send(NoteOff(MODIFIER_NOTES[num], 0))
|
|
|
|
macropad.pixels.show()
|