|
| 1 | +# SPDX-FileCopyrightText: 2021 John Park for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +# Ableton Live Macropad Launcher |
| 4 | +# In Ableton, choose "Launchpad Mini Mk3" as controller with MacroPad 2040 as in and out |
| 5 | +import board |
| 6 | +from digitalio import DigitalInOut, Pull |
| 7 | +import keypad |
| 8 | +import displayio |
| 9 | +import terminalio |
| 10 | +import neopixel |
| 11 | +import rotaryio |
| 12 | +from adafruit_simplemath import constrain |
| 13 | +from adafruit_display_text import label |
| 14 | +from adafruit_debouncer import Debouncer |
| 15 | +import usb_midi |
| 16 | +import adafruit_midi |
| 17 | +from adafruit_midi.control_change import ControlChange |
| 18 | +from adafruit_midi.note_off import NoteOff |
| 19 | +from adafruit_midi.note_on import NoteOn |
| 20 | +from adafruit_midi.midi_message import MIDIUnknownEvent |
| 21 | + |
| 22 | +TITLE_TEXT = "Live Launcher 2040" |
| 23 | +print(TITLE_TEXT) |
| 24 | +TRACK_NAMES = ["DRUM", "BASS", "SYNTH"] # Customize these |
| 25 | +LIVE_CC_NUMBER = 74 # CC number to send w encoder |
| 26 | +FADER_TEXT = "cutoff" # change for intended CC name |
| 27 | + |
| 28 | + |
| 29 | +midi = adafruit_midi.MIDI( |
| 30 | + midi_in=usb_midi.ports[0], |
| 31 | + in_channel=(0, 1, 2), |
| 32 | + midi_out=usb_midi.ports[1], |
| 33 | + out_channel=0 |
| 34 | +) |
| 35 | + |
| 36 | +# ---Specify key pins--- |
| 37 | +key_pins = ( |
| 38 | + board.KEY1, board.KEY2, board.KEY3, |
| 39 | + board.KEY4, board.KEY5, board.KEY6, |
| 40 | + board.KEY7, board.KEY8, board.KEY9, |
| 41 | + board.KEY10, board.KEY11, board.KEY12 |
| 42 | +) |
| 43 | +# ---Create keypad object--- |
| 44 | +keys = keypad.Keys(key_pins, value_when_pressed=False, pull=True) |
| 45 | + |
| 46 | +# ---Official Launchpad colors--- |
| 47 | +LP_COLORS = ( |
| 48 | + 0x000000, 0x101010, 0x202020, 0x3f3f3f, 0x3f0f0f, 0x3f0000, 0x200000, 0x100000, |
| 49 | + 0x3f2e1a, 0x3f0f00, 0x200800, 0x100400, 0x3f2b0b, 0x3f3f00, 0x202000, 0x101000, |
| 50 | + 0x213f0c, 0x143f00, 0x0a2000, 0x051000, 0x123f12, 0x003f00, 0x002000, 0x001000, |
| 51 | + 0x123f17, 0x003f06, 0x002003, 0x001001, 0x123f16, 0x003f15, 0x00200b, 0x001006, |
| 52 | + 0x123f2d, 0x003f25, 0x002012, 0x001009, 0x12303f, 0x00293f, 0x001520, 0x000b10, |
| 53 | + 0x12213f, 0x00153f, 0x000b20, 0x000610, 0x0b093f, 0x00003f, 0x000020, 0x000010, |
| 54 | + 0x1a0d3e, 0x0b003f, 0x060020, 0x030010, 0x3f0f3f, 0x3f003f, 0x200020, 0x100010, |
| 55 | + 0x3f101b, 0x3f0014, 0x20000a, 0x100005, 0x3f0300, 0x250d00, 0x1d1400, 0x080d01, |
| 56 | + 0x000e00, 0x001206, 0x00051b, 0x00003f, 0x001113, 0x040032, 0x1f1f1f, 0x070707, |
| 57 | + 0x3f0000, 0x2e3f0b, 0x2b3a01, 0x183f02, 0x032200, 0x003f17, 0x00293f, 0x000a3f, |
| 58 | + 0x06003f, 0x16003f, 0x2b061e, 0x0a0400, 0x3f0c00, 0x213701, 0x1c3f05, 0x003f00, |
| 59 | + 0x0e3f09, 0x153f1b, 0x0d3f32, 0x16223f, 0x0c1430, 0x1a1439, 0x34073f, 0x3f0016, |
| 60 | + 0x3f1100, 0x2d2900, 0x233f00, 0x201601, 0x0e0a00, 0x001203, 0x031308, 0x05050a, |
| 61 | + 0x050716, 0x190e06, 0x200000, 0x36100a, 0x351204, 0x3f2f09, 0x27370b, 0x192c03, |
| 62 | + 0x05050b, 0x36341a, 0x1f3a22, 0x26253f, 0x23193f, 0x0f0f0f, 0x1c1c1c, 0x373f3f, |
| 63 | + 0x270000, 0x0d0000, 0x063300, 0x011000, 0x2d2b00, 0x0f0c00, 0x2c1400, 0x120500, |
| 64 | +) |
| 65 | + |
| 66 | +LP_PADS = { |
| 67 | + 81: 0, 82: 1, 83: 2, |
| 68 | + 71: 3, 72: 4, 73: 5, |
| 69 | + 61: 6, 62: 7, 63: 8, |
| 70 | + 51: 9, 52: 10, 53: 11 |
| 71 | +} |
| 72 | + |
| 73 | +LIVE_NOTES = [81, 82, 83, 71, 72, 73, 61, 62, 63, 51, 52, 53] |
| 74 | +CC_OFFSET = 20 |
| 75 | +modifier = False # use to add encoder switch modifier to keys for clip mute |
| 76 | +MODIFIER_NOTES = [41, 42, 43, 41, 42, 43, 41, 42, 43, 41, 42, 43] # blank row in Live |
| 77 | + |
| 78 | +# ---Encoder setup--- |
| 79 | +encoder = rotaryio.IncrementalEncoder(board.ENCODER_B, board.ENCODER_A) |
| 80 | +sw = DigitalInOut(board.ENCODER_SWITCH) |
| 81 | +sw.switch_to_input(pull=Pull.UP) |
| 82 | +switch = Debouncer(sw) |
| 83 | +last_position = 0 # encoder position state |
| 84 | + |
| 85 | +# ---NeoPixel setup--- |
| 86 | +BRIGHT = 0.125 |
| 87 | +DIM = 0.0625 |
| 88 | +pixels = neopixel.NeoPixel(board.NEOPIXEL, 12, brightness=BRIGHT, auto_write=False) |
| 89 | + |
| 90 | +# ---Display setup--- |
| 91 | +display = board.DISPLAY |
| 92 | +screen = displayio.Group(max_size=12) |
| 93 | +display.show(screen) |
| 94 | +WIDTH = 128 |
| 95 | +HEIGHT = 64 |
| 96 | +FONT = terminalio.FONT |
| 97 | +# Draw a title label |
| 98 | +title = TITLE_TEXT |
| 99 | +title_area = label.Label(FONT, text=title, color=0xFFFFFF,x=6, y=3) |
| 100 | +screen.append(title_area) |
| 101 | + |
| 102 | +# --- create display strings and positions |
| 103 | +x1 = 5 |
| 104 | +x2 = 35 |
| 105 | +x3 = 65 |
| 106 | +y1 = 17 |
| 107 | +y2 = 27 |
| 108 | +y3 = 37 |
| 109 | +y4 = 47 |
| 110 | +y5 = 57 |
| 111 | + |
| 112 | +# ---Push knob text setup |
| 113 | +push_text_area = label.Label(FONT, text="[o]", color=0xffffff, x=WIDTH-22, y=y2) |
| 114 | +screen.append(push_text_area) |
| 115 | + |
| 116 | +# ---CC knob text setup |
| 117 | +fader_text_area = label.Label(FONT, text=FADER_TEXT, color=0xffffff, x=WIDTH - 42, y=y4) |
| 118 | +screen.append(fader_text_area) |
| 119 | +# --- cc value display |
| 120 | +cc_val_text = str(CC_OFFSET) |
| 121 | +cc_val_text_area = label.Label(FONT, text=cc_val_text, color=0xffffff, x=WIDTH - 20, y=y5) |
| 122 | +screen.append(cc_val_text_area) |
| 123 | + |
| 124 | +label_data = ( |
| 125 | + # text, x, y |
| 126 | + (TRACK_NAMES[0], x1, y1), (TRACK_NAMES[1], x2, y1), (TRACK_NAMES[2], x3, y1), |
| 127 | + (".", x1, y2), (".", x2, y2), (".", x3, y2), |
| 128 | + (".", x1, y3), (".", x2, y3), (".", x3, y3), |
| 129 | + (".", x1, y4), (".", x2, y4), (".", x3, y4), |
| 130 | + (".", x1, y5), (".", x2, y5), (".", x3, y5) |
| 131 | +) |
| 132 | + |
| 133 | +labels = [] |
| 134 | + |
| 135 | +for data in label_data: |
| 136 | + text, x, y = data |
| 137 | + label_area = label.Label(FONT, text=text, color=0xffffff) |
| 138 | + group = displayio.Group(max_size=4, x=x, y=y) |
| 139 | + group.append(label_area) |
| 140 | + screen.append(group) |
| 141 | + labels.append(label_area) # these can be individually addressed later |
| 142 | + |
| 143 | +num = 1 |
| 144 | + |
| 145 | +while True: |
| 146 | + msg_in = midi.receive() |
| 147 | + if isinstance(msg_in, NoteOn) and msg_in.velocity != 0: |
| 148 | + print( |
| 149 | + "received NoteOn", |
| 150 | + "from channel", |
| 151 | + msg_in.channel + 1, |
| 152 | + "MIDI note", |
| 153 | + msg_in.note, |
| 154 | + "velocity", |
| 155 | + msg_in.velocity, |
| 156 | + "\n" |
| 157 | + ) |
| 158 | + # send neopixel lightup code to key, text to display |
| 159 | + if msg_in.note < 84 and msg_in.note > 50: |
| 160 | + pixels[LP_PADS[msg_in.note]] = LP_COLORS[msg_in.velocity] |
| 161 | + pixels.show() |
| 162 | + if msg_in.velocity == 21: # active pad is indicated by Live as vel 21 |
| 163 | + labels[LP_PADS[msg_in.note]+3].text = "o" |
| 164 | + else: |
| 165 | + labels[LP_PADS[msg_in.note]+3].text = "." |
| 166 | + |
| 167 | + elif ( |
| 168 | + isinstance(msg_in, NoteOff) |
| 169 | + or isinstance(msg_in, NoteOn) |
| 170 | + and msg_in.velocity == 0 |
| 171 | + ): |
| 172 | + print( |
| 173 | + "received NoteOff", |
| 174 | + "from channel", |
| 175 | + msg_in.channel + 1, |
| 176 | + "MIDI note", |
| 177 | + msg_in.note, |
| 178 | + "velocity", |
| 179 | + msg_in.velocity, |
| 180 | + "\n" |
| 181 | + ) |
| 182 | + |
| 183 | + elif isinstance(msg_in, ControlChange): |
| 184 | + print( |
| 185 | + "received CC", |
| 186 | + "from channel", |
| 187 | + msg_in.channel + 1, |
| 188 | + "controller", |
| 189 | + msg_in.control, |
| 190 | + "value", |
| 191 | + msg_in.value, |
| 192 | + "\n" |
| 193 | + ) |
| 194 | + |
| 195 | + elif isinstance(msg_in, MIDIUnknownEvent): |
| 196 | + # Message are only known if they are imported |
| 197 | + print("Unknown MIDI event status ", msg_in.status) |
| 198 | + |
| 199 | + elif msg_in is not None: |
| 200 | + midi.send(msg_in) |
| 201 | + |
| 202 | + event = keys.events.get() # check for keypad events |
| 203 | + |
| 204 | + if not event: # Event is None; no keypad event happened, do other stuff |
| 205 | + |
| 206 | + position = encoder.position # store encoder position state |
| 207 | + cc_position = constrain((position + CC_OFFSET), 0, 127) # lock to cc range |
| 208 | + if last_position is None or position != last_position: |
| 209 | + |
| 210 | + if position < last_position: |
| 211 | + midi.send(ControlChange(LIVE_CC_NUMBER, cc_position)) |
| 212 | + print("CC", cc_position) |
| 213 | + cc_val_text_area.text = str(cc_position) |
| 214 | + |
| 215 | + elif position > last_position: |
| 216 | + midi.send(ControlChange(LIVE_CC_NUMBER, cc_position)) |
| 217 | + print("CC", cc_position) |
| 218 | + cc_val_text_area.text = str(cc_position) |
| 219 | + last_position = position |
| 220 | + |
| 221 | + switch.update() # check the encoder switch w debouncer |
| 222 | + if switch.fell: |
| 223 | + print("Mod") |
| 224 | + push_text_area.text = "[.]" |
| 225 | + modifier = True |
| 226 | + pixels.brightness = DIM |
| 227 | + pixels.show() |
| 228 | + |
| 229 | + if switch.rose: |
| 230 | + modifier = False |
| 231 | + push_text_area.text = "[o]" |
| 232 | + pixels.brightness = BRIGHT |
| 233 | + pixels.show() |
| 234 | + |
| 235 | + continue |
| 236 | + |
| 237 | + num = event.key_number |
| 238 | + |
| 239 | + if event.pressed and not modifier: |
| 240 | + midi.send(NoteOn(LIVE_NOTES[num], 127)) |
| 241 | + print("\nsent note", LIVE_NOTES[num], "\n") |
| 242 | + |
| 243 | + if event.pressed and modifier: |
| 244 | + midi.send(NoteOn(MODIFIER_NOTES[num], 127)) |
| 245 | + |
| 246 | + if event.released and not modifier: |
| 247 | + midi.send(NoteOff(LIVE_NOTES[num], 0)) |
| 248 | + |
| 249 | + if event.released and modifier: |
| 250 | + midi.send(NoteOff(MODIFIER_NOTES[num], 0)) |
| 251 | + |
| 252 | + pixels.show() |
0 commit comments