Skip to content

Commit 8ec62bb

Browse files
committed
launchpad and mouse improvements
1 parent debab50 commit 8ec62bb

File tree

2 files changed

+178
-69
lines changed

2 files changed

+178
-69
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# linnstrument-wholetone
22

3-
Wholetone layout system and visualizer for the Linnstrument. I consider this to be one of the most accessible and playable musical note layouts, and once you learn how it works, you'll realize why I've made it my primary layout.
3+
Wholetone layout system and visualizer for the Linnstrument, Launchpad X, and computer keyboard. I consider this to be one of the most accessible and playable musical note layouts, and once you learn how it works, you'll realize why I've made it my primary layout.
44

55
Note: So far, this has only been tested on the LinnStrument 128 version. If you own the 200-note version,
66
please let me know how this works for you.

app.py

Lines changed: 177 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,13 @@
8585

8686
class Note:
8787
def __init__(self):
88-
self.bend = 0.0
89-
self.pressed = False
90-
self.intensity = 0.0 # how much to light marker up
88+
# self.bend = 0.0
89+
# self.pressed = False
90+
# self.intensity = 0.0 # how much to light marker up (in app)
9191
self.pressure = 0.0 # how much the note is being pressed
92-
self.dirty = False
92+
# self.dirty = False
9393
self.location: ivec2 = None # on board
94+
self.midinote = None
9495

9596
# def logic(self, dt):
9697
# if self.pressed: # pressed, fade to pressure value
@@ -257,15 +258,8 @@ def get_color(self, x, y):
257258
def mouse_held(self):
258259
return self.mouse_midi != -1
259260

260-
def mouse_press(self, x, y, state=True, hold=False):
261-
if y < 0:
262-
return
263-
264-
# if we're not intending to hold the note, we release the previous primary note
265-
if not hold:
266-
if self.mouse_held():
267-
self.mouse_release()
268-
261+
# layout button x, y and velocity
262+
def mouse_pos_to_press(self, x, y):
269263
vel = y % int(self.button_sz)
270264
x /= int(self.button_sz)
271265
y /= int(self.button_sz)
@@ -276,13 +270,53 @@ def mouse_press(self, x, y, state=True, hold=False):
276270
vel = clamp(0, 127, int(vel))
277271

278272
x, y = int(x), int(y)
273+
return (x, y, vel)
279274

280-
self.mark_xy(x, y, state)
275+
def mouse_press(self, x, y, state=True, hold=False, hover=False):
276+
if y < 0:
277+
return
278+
279+
if hover:
280+
btn = pygame.mouse.get_pressed(3)[0]
281+
if not btn:
282+
return
283+
284+
# if we're not intending to hold the note, we release the previous primary note
285+
if not hover:
286+
if self.mouse_held():
287+
self.mouse_release()
288+
289+
x, y, vel = self.mouse_pos_to_press(x, y)
290+
291+
if hover and self.mouse_midi_vel is not None:
292+
# if hovering, get velocity of last click
293+
vel = self.mouse_midi_vel
294+
if not hover and self.mouse_midi_vel is None:
295+
self.mouse_midi_vel = vel # store velocity for initial click
296+
297+
# vel = y % int(self.button_sz)
298+
# x /= int(self.button_sz)
299+
# y /= int(self.button_sz)
300+
301+
# vel = vel / int(self.button_sz)
302+
# vel = 1 - vel
303+
# vel *= 127
304+
# vel = clamp(0, 127, int(vel))
305+
306+
# x, y = int(x), int(y)
281307
v = ivec2(x, y)
308+
309+
self.mark_xy(x, y, state)
282310
midinote = self.xy_to_midi(v.x, v.y)
311+
if hover:
312+
if self.mouse_midi == midinote:
313+
return
314+
else:
315+
self.mouse_release()
283316
if not hold:
284317
self.mouse_mark = v
285318
self.mouse_midi = midinote
319+
self.mouse_midi_vel = vel
286320
data = [0x90 if state else 0x80, midinote, vel]
287321
if self.midi_out:
288322
self.midi_write(self.midi_out, data, 0)
@@ -302,6 +336,9 @@ def mouse_release(self, x=None, y=None):
302336
self.midi_write(self.midi_out, data, 0)
303337
self.mouse_midi = -1
304338

339+
def mouse_hover(self, x, y):
340+
self.mouse_press(x, y, hover=True)
341+
305342
# Given an x,y position, find the octave
306343
# (used to initialize octaves 2D array)
307344
def init_octave(self, x, y):
@@ -336,6 +373,12 @@ def midi_write(self, dev, msg, ts=0):
336373
if dev:
337374
dev.send_raw(*msg)
338375

376+
def next_free_note(self):
377+
for note in self.notes:
378+
if note.location is None:
379+
return note
380+
return None
381+
339382
def note_on(self, data, timestamp, width=None, curve=True, no_overlap=None):
340383
if width is None:
341384
width = self.board_w // 2 if self.options.hardware_split else self.board_w
@@ -350,9 +393,6 @@ def note_on(self, data, timestamp, width=None, curve=True, no_overlap=None):
350393
data[0] = d0 & 0xF0 # send all to channel 0 if enabled
351394
row = None
352395
col = None
353-
if not no_overlap:
354-
row = ch % 8
355-
col = ch // 8
356396

357397
if no_overlap:
358398
row = data[1] // width
@@ -363,6 +403,8 @@ def note_on(self, data, timestamp, width=None, curve=True, no_overlap=None):
363403
if self.options.hardware_split and ch >= 8:
364404
data[1] += width * 2
365405
else:
406+
row = ch % 8
407+
col = ch // 8
366408
data[1] *= 2
367409
try:
368410
data[1] -= row * 5
@@ -392,12 +434,22 @@ def note_on(self, data, timestamp, width=None, curve=True, no_overlap=None):
392434
int(vel * 127 + 0.5),
393435
)
394436

395-
note = self.notes[ch]
396-
if note.location is None:
397-
note.location = ivec2(0)
398-
self.notes[ch].location.x = col
399-
self.notes[ch].location.y = row
400-
self.notes[ch].pressure = vel
437+
if aftertouch:
438+
# TODO: add aftertouch values into notes array
439+
# This is not necessary yet
440+
pass
441+
else:
442+
if self.options.no_overlap:
443+
note = self.notes[ch]
444+
else:
445+
note = self.next_free_note()
446+
if note:
447+
if note.location is None:
448+
note.location = ivec2(0)
449+
note.location.x = col
450+
note.location.y = row
451+
note.pressure = vel
452+
note.midinote = data[1]
401453

402454
if self.is_split():
403455
if split_chan == 0:
@@ -538,32 +590,70 @@ def cb_foot(self, data, timestamp):
538590
high = self.options.velocity_curve_high
539591
self.velocity_curve_ = low + val2 * (high - low)
540592

541-
def cb_launchpad_in(self, event, timestamp):
542-
if event[0] == 144:
543-
# convert to x, y (lower left is 0, 0)
544-
y = event[1] // 10 - 1
545-
x = event[1] % 10 - 1
546-
# convert it to no overlap chromatic
547-
548-
self.launchpad_state[y][x] = None
549-
note = y * 8 + x
550-
self.note_off([128, note, event[2]], timestamp, width=8, no_overlap=True)
551-
elif event[0] == 160:
552-
y = event[1] // 10 - 1
553-
x = event[1] % 10 - 1
554-
state = self.launchpad_state[y][x]
555-
self.launchpad_state[y][x] = event[2]
593+
# uses button state events (mk3 pro)
594+
def cb_launchpad_in(self, event, timestamp=0):
595+
# if (self.launchpad_mode == "pro" or self.launchpad_mode == "promk3") and event[0] == 255:
596+
# if event[0] >= 255:
597+
# # I'm testing the mk3 method on an lpx, so I'll check this here
598+
# vel = event[2] if self.launchpad_mode == 'lpx' else event[1]
599+
# for note in self.notes:
600+
# if note.location:
601+
# print(note.midinote)
602+
# self.note_on([160, note.midinote, vel], timestamp, width=8, no_overlap=True)
603+
604+
if self.launchpad_mode == 'lpx' and event[0] >= 255: # pressure
605+
x = event[0] - 255
606+
y = 8 - (event[1] - 255)
607+
vel = event[2]
556608
note = y * 8 + x
557-
if state is None: # just pressed
558-
self.note_on([144, note, event[2]], timestamp, width=8, no_overlap=True, curve=False)
559-
self.note_on([160, note, event[2]], timestamp, width=8, no_overlap=True, curve=False)
560-
elif event[0] == 176:
561-
if event == [176, 93, 127, 0]:
562-
self.transpose_board(-1)
563-
self.dirty = self.dirty_lights = True
564-
elif event == [176, 94, 127, 0]:
565-
self.transpose_board(1)
566-
self.dirty = self.dirty_lights = True
609+
self.note_on([160, note, event[2]], timestamp, width=8, no_overlap=True)
610+
elif event[2] == 0: # note off
611+
x = event[0]
612+
y = 8 - event[1]
613+
if 0 <= x < 8 and 0 <= y < 8:
614+
note = y * 8 + x
615+
self.note_off([128, note, event[2]], timestamp, width=8, no_overlap=True)
616+
else: # note on
617+
x = event[0]
618+
y = 8 - event[1]
619+
if 0 <= x < 8 and 0 <= y < 8:
620+
note = y * 8 + x
621+
self.note_on([144, note, event[2]], timestamp, width=8, no_overlap=True)
622+
else:
623+
if x == 2:
624+
self.transpose_board(-1)
625+
self.dirty = self.dirty_lights = True
626+
elif x == 3:
627+
self.transpose_board(1)
628+
self.dirty = self.dirty_lights = True
629+
630+
# uses raw events (Launchpad X)
631+
# def cb_launchpad_in(self, event, timestamp=0):
632+
# if event[0] == 144:
633+
# # convert to x, y (lower left is 0, 0)
634+
# y = event[1] // 10 - 1
635+
# x = event[1] % 10 - 1
636+
# # convert it to no overlap chromatic
637+
638+
# self.launchpad_state[y][x] = None
639+
# note = y * 8 + x
640+
# self.note_off([128, note, event[2]], timestamp, width=8, no_overlap=True)
641+
# elif event[0] == 160:
642+
# y = event[1] // 10 - 1
643+
# x = event[1] % 10 - 1
644+
# state = self.launchpad_state[y][x]
645+
# self.launchpad_state[y][x] = event[2]
646+
# note = y * 8 + x
647+
# if state is None: # just pressed
648+
# self.note_on([144, note, event[2]], timestamp, width=8, no_overlap=True, curve=False)
649+
# self.note_on([160, note, event[2]], timestamp, width=8, no_overlap=True, curve=False)
650+
# elif event[0] == 176:
651+
# if event == [176, 93, 127, 0]:
652+
# self.transpose_board(-1)
653+
# self.dirty = self.dirty_lights = True
654+
# elif event == [176, 94, 127, 0]:
655+
# self.transpose_board(1)
656+
# self.dirty = self.dirty_lights = True
567657

568658
# if events[0] >= 255:
569659
# print("PRESSURE: " + str(events[0]-255) + " " + str(events[1]))
@@ -702,6 +792,7 @@ def __init__(self):
702792
self.options.width = get_option(opts, "width", DEFAULT_OPTIONS.width)
703793

704794
self.options.launchpad = get_option(opts, 'launchpad', True)
795+
self.options.experimental = get_option(opts, 'experimental', False)
705796

706797
# simulator keys
707798
self.keys = {}
@@ -755,6 +846,9 @@ def __init__(self):
755846

756847
self.mouse_mark = ivec2(0)
757848
self.mouse_midi = -1
849+
self.mouse_midi_vel = None
850+
851+
self.last_note = None # ivec2
758852

759853
self.init_board()
760854

@@ -900,6 +994,7 @@ def __init__(self):
900994
self.midi_in = None
901995
self.visualizer = None
902996
self.launchpad = None
997+
self.launchpad_mode = None
903998

904999
innames = rtmidi2.get_in_ports()
9051000
for i in range(len(innames)):
@@ -922,22 +1017,23 @@ def __init__(self):
9221017
self.foot_in.callback = self.cb_foot
9231018

9241019
if self.options.launchpad:
925-
# lp = launchpad.LaunchpadPro()
926-
# if lp.Check(0):
927-
# if lp.Open(0):
928-
# mode = "pro"
929-
# elif launchpad.LaunchpadProMk3().Check(0):
930-
# lp = launchpad.LaunchpadProMk3()
931-
# if lp.Open(0):
932-
# mode = "promk3"
933-
# elif
934-
if launchpad.LaunchpadLPX().Check(1):
935-
lp = launchpad.LaunchpadLPX()
936-
if lp.Open(1):
937-
mode = "lpx"
938-
if mode is not None:
1020+
lp = None
1021+
if self.options.experimental:
1022+
lp = launchpad.LaunchpadPro()
1023+
if lp.Check(0):
1024+
if lp.Open(0):
1025+
self.launchpad_mode = "pro"
1026+
if launchpad.LaunchpadProMk3().Check(0):
1027+
lp = launchpad.LaunchpadProMk3()
1028+
if lp.Open(0):
1029+
self.launchpad_mode = "promk3"
1030+
if not self.launchpad_mode:
1031+
if launchpad.LaunchpadLPX().Check(1):
1032+
lp = launchpad.LaunchpadLPX()
1033+
if lp.Open(1):
1034+
self.launchpad_mode = "lpx"
1035+
if self.launchpad_mode is not None:
9391036
self.launchpad = lp
940-
# self.init_launchpad()
9411037

9421038
self.done = False
9431039

@@ -1020,7 +1116,10 @@ def mark_xy(self, x, y, state, use_lights=False):
10201116
def mark(self, midinote, state, use_lights=False, only_row=None):
10211117
if only_row is not None:
10221118
only_row = self.board_h - only_row - 1 - self.flipped # flip
1023-
rows = [self.board[only_row]]
1119+
try:
1120+
rows = [self.board[only_row]]
1121+
except IndexError:
1122+
rows = self.board
10241123
y = only_row
10251124
else:
10261125
rows = self.board
@@ -1068,11 +1167,17 @@ def logic(self, dt):
10681167
# keys = pygame.key.get_pressed()
10691168

10701169
if self.launchpad:
1170+
# while True:
1171+
# events = self.launchpad.EventRaw()
1172+
# if events != []:
1173+
# for ev in events:
1174+
# self.cb_launchpad_in(ev[0], ev[1])
1175+
# else:
1176+
# break
10711177
while True:
1072-
events = self.launchpad.EventRaw()
1073-
if events != []:
1074-
for ev in events:
1075-
self.cb_launchpad_in(ev[0], ev[1])
1178+
event = self.launchpad.ButtonStateXY(returnPressure = True)
1179+
if event:
1180+
self.cb_launchpad_in(event)
10761181
else:
10771182
break
10781183

@@ -1107,6 +1212,10 @@ def logic(self, dt):
11071212
self.midi_write(self.midi_out, data, 0)
11081213
except KeyError:
11091214
pass
1215+
elif ev.type == pygame.MOUSEMOTION:
1216+
x, y = ev.pos
1217+
y -= self.menu_sz
1218+
self.mouse_hover(x, y)
11101219
elif ev.type == pygame.MOUSEBUTTONDOWN:
11111220
x, y = ev.pos
11121221
y -= self.menu_sz

0 commit comments

Comments
 (0)