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()