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