Skip to content

Commit c5892cf

Browse files
committed
Added second launchpad vibrato mode (pitch bend), reduced midi messages, gamepad experiment, improved mpe setup
1 parent aecca88 commit c5892cf

7 files changed

Lines changed: 177 additions & 20 deletions

File tree

midimech.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from src.note import Note
1515
from src.settings import DeviceSettings
1616
from src.core import Core
17+
from src.gamepad import Gamepad
1718

1819
with open(os.devnull, "w") as devnull:
1920
# suppress pygame messages

src/articulation.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from enum import Enum
2-
from src.util import sign
2+
from src.util import *
33
from src.constants import *
44

55
class Articulation:
@@ -8,7 +8,7 @@ class Articulation:
88
def __init__(self, core):
99
self.core = core
1010
self.value = 0.0
11-
self.time_between_ticks = 0.05
11+
self.time_between_ticks = 0.02
1212
self.timer = 0.0
1313
self.state = self.State(self.State.off)
1414

@@ -27,6 +27,16 @@ def __init__(self, core):
2727

2828
self.mod = 0.0
2929
self.mod_changed = True
30+
self.last_midi_message = None
31+
32+
self.mode = self.core.options.vibrato
33+
if self.mode not in ('mod', 'pitch'):
34+
print('Vibrato option must be mod or pitch. Using mod.')
35+
self.mode = 'mod'
36+
self.t = 0.0
37+
self.vibrato_speed = 5.0
38+
self.vibrato_depth = 1.0 / 4.0
39+
self.vibrato_shift = 0.0 # 0.1
3040

3141
def pressure(self, value):
3242
if not value or value <= 0.0:
@@ -88,7 +98,24 @@ def tick(self):
8898
# return
8999

90100
if self.mod_changed:
91-
self.core.midi_write(self.core.midi_out, [0xb0, 1, int(self.mod * 127)])
101+
if self.mode == 'mod':
102+
msg = [0xb0, 1, int(self.mod * 127)]
103+
if msg != self.last_midi_message:
104+
self.core.midi_write(self.core.midi_out, msg)
105+
self.last_midi_message = msg
106+
elif self.mode == 'pitch':
107+
if self.mod > 0.0:
108+
wave = self.mod * math.sin(math.tau * self.t * self.vibrato_speed) * self.vibrato_depth + self.vibrato_shift
109+
pb = compose_pitch_bend(wave)
110+
msg = [0xe0, pb[0], pb[1]]
111+
if msg != self.last_midi_message:
112+
self.core.midi_write(self.core.midi_out, msg)
113+
self.last_midi_message = msg
114+
else:
115+
msg = [0xe0, 0, 0x40]
116+
if msg != self.last_midi_message:
117+
self.core.midi_write(self.core.midi_out, msg)
118+
self.last_midi_message = msg
92119
self.mod_changed = False
93120

94121
# held_note_count = self.core.held_note_count()
@@ -97,6 +124,7 @@ def tick(self):
97124

98125
def logic(self, dt):
99126
self.timer += dt
127+
self.t += dt
100128

101129
self.vibrato_window_t = max(0.0, self.vibrato_window_t - 1.0 * dt)
102130
if self.vibrato_window_t <= 0.0:

src/core.py

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from src.note import Note
1515
from src.settings import DeviceSettings
1616
from src.articulation import Articulation
17+
from src.gamepad import Gamepad
1718

1819
with open(os.devnull, "w") as devnull:
1920
# suppress pygame messages
@@ -122,12 +123,12 @@ def set_light(self, x, y, col): # col is [1,11], 0 resets
122123
self.send_cc(0, 22, col)
123124

124125
if self.launchpad:
125-
lp_col = COLOR_CODES[col]
126+
lp_col = COLOR_CODES[col] / 4
126127
if 0 <= x < 8 and 0 <= y < 8:
127128
if not self.is_macro_button(x, y):
128129
self.launchpad.LedCtrlXY(x, y+1, lp_col[0], lp_col[1], lp_col[2])
129130
else:
130-
self.launchpad.LedCtrlXY(x, y+1, 127, 127, 127)
131+
self.launchpad.LedCtrlXY(x, y+1, 63, 63, 63)
131132

132133
def reset_light(self, x, y, reset_red=True):
133134
note = self.get_note_index(x, y)
@@ -176,16 +177,16 @@ def reset_launchpad_light(self, x, y):
176177
def set_red_light(self, x, y, state=True):
177178
self.red_lights[y][x] = state
178179
if self.launchpad:
179-
lp_col = ivec3(127, 0, 0)
180+
lp_col = ivec3(63, 0, 0)
180181
if state:
181182
self.launchpad.LedCtrlXY(x, y, lp_col[0], lp_col[1], lp_col[2])
182183

183184
def set_launchpad_light(self, x, y, color):
184-
col = COLOR_CODES[color]
185+
col = COLOR_CODES[color] / 4
185186
if not self.is_macro_button(x, 8 - y - 1):
186187
self.launchpad.LedCtrlXY(x, 8-y, col[0], col[1], col[2])
187188
else:
188-
self.launchpad.LedCtrlXY(x, 8-y, 127, 127, 127)
189+
self.launchpad.LedCtrlXY(x, 8-y, 63, 63, 63)
189190

190191
def setup_lights(self):
191192
for y in range(self.board_h):
@@ -397,6 +398,7 @@ def init_board(self):
397398
self.notes[i] = Note()
398399

399400
self.chord_notes = [False] * 127
401+
self.note_set = set()
400402

401403
def midi_write(self, dev, msg, ts=0):
402404
if dev:
@@ -514,6 +516,7 @@ def note_on(self, data, timestamp, width=None, curve=True, mpe=None):
514516
note.split = split_chan
515517

516518
self.chord_notes[data[1]] = True
519+
self.note_set.add(data[1])
517520
self.dirty_chord = True
518521

519522
if self.is_split():
@@ -610,6 +613,7 @@ def note_off(self, data, timestamp, width=None, mpe=None):
610613
data[1] += 7
611614

612615
self.chord_notes[data[1]] = False
616+
self.note_set.remove(data[1])
613617
self.dirty_chord = True
614618

615619
if self.is_split():
@@ -806,16 +810,14 @@ def macro(self, x, y, val):
806810

807811
# uses button state events (mk3 pro)
808812
def cb_launchpad_in(self, event, timestamp=0):
809-
# if (self.launchpad_mode == "pro" or self.launchpad_mode == "promk3") and event[0] == 255:
810-
# if event[0] >= 255:
811-
# # I'm testing the mk3 method on an lpx, so I'll check this here
812-
# vel = event[2] if self.launchpad_mode == 'lpx' else event[1]
813-
# for note in self.notes:
814-
# if note.location:
815-
# print(note.midinote)
816-
# self.note_on([160, note.midinote, vel], timestamp, width=8, mpe=True)
817-
818-
if self.launchpad_mode == 'lpx' and event[0] >= 255: # pressure
813+
if (self.launchpad_mode == "pro" or self.launchpad_mode == "promk3") and event[0] >= 255:
814+
# if event[0] >= 255: # uncomment this for testing pro behavior on launchpad X
815+
# I'm testing the mk3 method on an lpx, so I'll check this here
816+
vel = event[2] if self.launchpad_mode == 'lpx' else event[1]
817+
for note in self.note_set:
818+
self.midi_write(self.midi_out, [160, note, vel], timestamp)
819+
self.articulation.pressure(vel / 127)
820+
elif self.launchpad_mode == 'lpx' and event[0] >= 255: # pressure
819821
x = event[0] - 255
820822
y = 8 - (event[1] - 255)
821823
vel = event[2]
@@ -1053,6 +1055,8 @@ def __init__(self):
10531055
# ) or get_option(
10541056
# opts, "no_overlap", DEFAULT_OPTIONS.mpe
10551057
# )
1058+
self.options.mpe = True
1059+
self.options.vibrato = get_option(opts, "vibrato", DEFAULT_OPTIONS.vibrato)
10561060
self.options.midi_out = get_option(opts, "midi_out", DEFAULT_OPTIONS.midi_out)
10571061
self.options.split_out = get_option(
10581062
opts, "split_out", DEFAULT_OPTIONS.split_out
@@ -1358,6 +1362,13 @@ def __init__(self):
13581362
self.visualizer = None
13591363
self.launchpad = None
13601364
self.launchpad_mode = None
1365+
1366+
self.gamepad = None
1367+
if self.options.experimental:
1368+
pygame.joystick.init()
1369+
if pygame.joystick.get_count() > 0:
1370+
self.gamepad = Gamepad(self, 0)
1371+
print('Gamepad initialized')
13611372

13621373
innames = rtmidi2.get_in_ports()
13631374
for i in range(len(innames)):
@@ -1469,13 +1480,35 @@ def rpn(self, num, value):
14691480
self.midi_write(self.linn_out, [176, 38, value_lsb])
14701481
self.midi_write(self.linn_out, [176, 101, 127])
14711482
self.midi_write(self.linn_out, [176, 100, 127])
1472-
time.sleep(0.1)
1483+
time.sleep(0.05)
14731484

14741485
def mpe_rpn(self, on=True):
14751486
if on:
14761487
self.rpn(0, 1)
14771488
self.rpn(100, 1)
14781489
self.rpn(227, 0)
1490+
1491+
if self.options.hardware_split:
1492+
# left side channels
1493+
self.rpn(1, 1)
1494+
self.rpn(2, 0)
1495+
for x in range(3, 10):
1496+
self.rpn(x, 1)
1497+
for x in range(10, 18):
1498+
self.rpn(x, 0)
1499+
1500+
# right side channels
1501+
self.rpn(101, 16) # main=16
1502+
for x in range(110, 117):
1503+
self.rpn(x, 1)
1504+
self.rpn(117, 0) # 16 off
1505+
else:
1506+
# left side only
1507+
self.rpn(1, 1)
1508+
self.rpn(2, 0)
1509+
for x in range(3, 18):
1510+
self.rpn(x, 1)
1511+
14791512
else:
14801513
self.rpn(227, 5)
14811514

@@ -1682,6 +1715,16 @@ def logic(self, dt):
16821715
except KeyError:
16831716
pass
16841717

1718+
else:
1719+
if self.gamepad and ev.type in (\
1720+
pygame.JOYAXISMOTION,
1721+
pygame.JOYBALLMOTION,
1722+
pygame.JOYBUTTONDOWN,
1723+
pygame.JOYBUTTONUP,
1724+
pygame.JOYHATMOTION
1725+
):
1726+
self.gamepad.event(ev)
1727+
16851728
if not self.options.lite:
16861729
if ev.type == pygame.MOUSEMOTION:
16871730
x, y = ev.pos
@@ -1777,6 +1820,9 @@ def logic(self, dt):
17771820
if self.launchpad:
17781821
self.articulation.logic(dt)
17791822

1823+
if self.gamepad:
1824+
self.gamepad.logic(dt)
1825+
17801826
# figure out the lowest note to highlight it
17811827
# if self.options.show_lowest_note:
17821828
# old_lowest_note = self.lowest_note
@@ -1992,6 +2038,31 @@ def render(self):
19922038
x += 1
19932039
y += 1
19942040

2041+
if self.gamepad:
2042+
pos = self.gamepad.positions()
2043+
# gp_pos.y = self.board_h - y - 1
2044+
2045+
circ = [None] * 2
2046+
for i in range(2):
2047+
circ[i] = ivec2(
2048+
int(pos[i].x * sz + b / 2 + sz / 2),
2049+
int(self.menu_sz + pos[i].y * sz + b / 2 + sz / 2),
2050+
)
2051+
2052+
pygame.gfxdraw.aacircle(
2053+
self.screen.surface,
2054+
circ[i].x + 1,
2055+
circ[i].y - 1,
2056+
rad,
2057+
ivec3(0, 255, 0),
2058+
)
2059+
# pygame.gfxdraw.filled_circle(
2060+
# self.screen.surface,
2061+
# circ[i].x + 1,
2062+
# circ[i].y - 1,
2063+
# rad,
2064+
# ivec3(0, 255, 0),
2065+
# )
19952066

19962067
# if self.options.experimental:
19972068
text = self.font.render(self.scale_name, True, ivec3(127))

src/gamepad.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import pygame
2+
from glm import ivec2, vec2, length
3+
from src.util import sign
4+
5+
class Gamepad:
6+
def __init__(self, core, num=0):
7+
self.core = core
8+
self.joy = pygame.joystick.Joystick(num)
9+
self.notes = [None] * 16
10+
for i, note in enumerate(self.notes):
11+
self.notes[i] = set()
12+
self.cursors = [ivec2(4, 5), ivec2(5, 2)]
13+
self.offset = [vec2(0, 0), vec2(0,0)]
14+
self.ioffset = [ivec2(0, 0), ivec2(0, 0)]
15+
self.deadzone = 0.1
16+
def positions(self):
17+
return [self.cursors[0] + self.ioffset[0], self.cursors[1] + self.ioffset[1]]
18+
def logic(self, dt):
19+
for ofs_idx in range(2):
20+
for axis, value in enumerate(self.offset[ofs_idx]):
21+
self.ioffset[ofs_idx][axis%2] = int(round(value))
22+
# fix corners
23+
if abs(self.offset[ofs_idx].x) > 0.6 and abs(self.offset[ofs_idx].y) > 0.6:
24+
self.ioffset[ofs_idx] = ivec2(sign(self.offset[ofs_idx].x), sign(self.offset[ofs_idx].y))
25+
# print(self.ioffset)
26+
self.core.dirty = True
27+
def play(self, ch, x, y, vel):
28+
note = y * 8 + x
29+
self.core.note_on([144, note, 100], 0, width=8, mpe=True)
30+
self.notes[ch].add(note)
31+
def event(self, ev):
32+
if ev.type == pygame.JOYAXISMOTION:
33+
axis = ev.axis
34+
# print(axis)
35+
if 2 <= axis < 4:
36+
self.offset[1][axis-2] = ev.value
37+
elif 0 <= axis < 2:
38+
self.offset[0][axis] = ev.value
39+
elif ev.type == pygame.JOYHATMOTION:
40+
self.cursors[0] += ivec2(ev.value[0], -ev.value[1])
41+
elif ev.type == pygame.JOYBUTTONUP:
42+
for note in self.notes[ev.button]:
43+
self.core.note_off([128, note, 100], 0, width=8, mpe=True)
44+
elif ev.type == pygame.JOYBUTTONDOWN:
45+
for note in self.notes[ev.button]:
46+
self.core.note_off([128, note, 100], 0, width=8, mpe=True)
47+
if ev.button in (0, 1):
48+
pos = self.positions()[ev.button]
49+
elif ev.button == 2:
50+
pos = self.positions()[0] + ivec2(2,0)
51+
elif ev.button == 3:
52+
pos = self.positions()[1] + ivec2(2,0)
53+
x = pos.x
54+
y = self.core.board_h - pos.y - 1
55+
self.play(ev.button, x, y, 100)
56+

src/note.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ def __init__(self):
33
# self.bend = 0.0
44
# self.pressed = False
55
# self.intensity = 0.0 # how much to light marker up (in app)
6+
self.ipressure = 0 # 0, 127
67
self.pressure = 0.0 # how much the note is being pressed
78
# self.dirty = False
89
self.location = None # on board

src/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Options:
2525
size: int = 128
2626
width: int = 16
2727
height: int = 8
28+
vibrato: str = 'mod'
2829

2930
DEFAULT_OPTIONS = Options()
3031

src/util.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ def decompose_pitch_bend(pitch_bend_bytes):
102102
pitch_bend_norm = (pitch_bend_value - 8192) / 8192.0
103103
return pitch_bend_norm
104104

105-
106105
def compose_pitch_bend(pitch_bend_norm):
107106
pitch_bend_value = int((pitch_bend_norm + 1.0) * 8192)
108107
pitch_bend_bytes = [pitch_bend_value & 0x7F, (pitch_bend_value >> 7) & 0x7F]

0 commit comments

Comments
 (0)