-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathaiolaunchpad.py
181 lines (134 loc) · 4.62 KB
/
aiolaunchpad.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""
AioLaunchpad is an Asyncio based framework to read and control the
Novation Launch MIDI controllers.
"""
import re
import asyncio
import aiofiles
# LEDs and inputs do not always have the same id mapping on the
# Launch Control XL MK2 (LCXL2). These numbers correspond to the
# User Template mode:
LCXL2_MAPPING = {
'lights': {
'knob': [13, 29, 45, 61, 77, 93, 109, 125,
14, 30, 46, 62, 78, 94, 110, 126,
15, 31, 47, 63, 79, 95, 111, 127],
'button': [41, 42, 43, 44, 57, 58, 59, 60,
73, 74, 75, 76, 89, 90, 91, 92],
'control': [105, 106, 107, 108],
'track_select': [106, 107],
'send_select': [104, 105],
},
'inputs': {
'knob': [13, 14, 15, 16, 17, 18, 19, 20,
29, 30, 31, 32, 33, 34, 35, 36,
49, 50, 51, 52, 53, 54, 55, 56],
'fader': [77, 78, 79, 80, 81, 82, 83, 84],
'button': [41, 42, 43, 44, 57, 58, 59, 60,
73, 74, 75, 76, 89, 90, 91, 92],
'control': [105, 106, 107, 108],
'track_select': [106, 107],
'send_select': [104, 105],
},
}
class Shortcut:
def __init__(self, mapping: dict):
self.mapping = mapping
def __getattr__(self, item):
"""Support for pad.input.button0, etc"""
match = re.match(r'(\w+)(\d+)', item)
if match:
name = match.group(1)
index = int(match.group(2))
return self.mapping[name][index]
raise AttributeError()
@property
def all(self):
for group in self.mapping.values():
for id in group:
yield id
def knobs(self, row=None):
ids = self.mapping['knob']
if row is None:
return self.mapping['knob']
else:
return ids[8 * row:8 * (row + 1)]
def buttons(self, row=None):
ids = self.mapping['button']
if row is None:
return self.mapping['button']
else:
return ids[8 * row:8 * (row + 1)]
class LightsShortcut(Shortcut):
pass
class InputsShortcut(Shortcut):
pressed = 144
released = 128
# ---
def parse_midi(midi):
"""Convert a MIDI (3 bytes) to a dictionnary for ease of use."""
return {
'code': midi[0], 'input': midi[1], 'value': midi[2],
}
def note_fits_annotations(note, annotations):
"""Return whether a note matches a function's annotations."""
for ann in annotations:
if note[ann] != annotations[ann]:
return False
return True
# --- Useful functions ---
async def set_color(color, light_id, device):
"""Set the color of a light given its id."""
"Set the color of a light given its id."
await device.write(bytes([0x90, light_id, color]))
def spin(task):
"""Run a task asynchronously in the event loop, shortcut"""
return asyncio.get_event_loop().create_task(task)
# --- Launch boards ---
class LaunchBoard:
def __init__(self, path='/dev/midi2'):
self.path = path
self.handlers = []
async def setup(self, device):
"""Turn of all lights during setup"""
for light in self.lights.all:
await set_color(0, light, device)
def register(self, handler):
"""Decorator"""
annotations = handler.__annotations__
async def subscriber(queue, device):
while True:
midi = await queue.get()
note = parse_midi(midi)
if note_fits_annotations(note, annotations):
await handler(**note, device=device)
self.handlers.append(subscriber)
return handler
@staticmethod
async def input_handler(device, queues):
while True:
midi = await device.read(3)
for queue in queues:
await queue.put(midi)
async def run(self):
queues = []
async with aiofiles.open(self.path, 'wb+', buffering=0) as device:
await self.setup(device)
tasks = []
for handler in self.handlers:
queue = asyncio.Queue()
queues.append(queue)
tasks.append(handler(queue, device))
await asyncio.gather(
self.input_handler(device, queues),
*tasks,
)
def run_app(self):
loop = asyncio.get_event_loop()
loop.run_until_complete(self.run())
loop.close()
class LaunchControlXL(LaunchBoard):
lights = LightsShortcut(LCXL2_MAPPING['lights'])
inputs = InputsShortcut(LCXL2_MAPPING['inputs'])
def __init__(self, path='/dev/midi2'):
super().__init__(path)