Refactor MIDI stuff so deck focusing is listenable and controllable
[SugarCubes.git] / _MIDI.pde
CommitLineData
d6ac1ee8
MS
1/**
2 * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND
3 *
4 * //\\ //\\ //\\ //\\
5 * ///\\\ ///\\\ ///\\\ ///\\\
6 * \\\/// \\\/// \\\/// \\\///
7 * \\// \\// \\// \\//
8 *
9 * EXPERTS ONLY!! EXPERTS ONLY!!
10 *
11 * This file defines the MIDI mapping interfaces. This shouldn't
12 * need editing unless you're adding top level support for a
13 * specific MIDI device of some sort. Generally, all MIDI devices
14 * will just work with the default configuration, and you can
15 * set your SCPattern class to respond to the controllers that you
16 * care about.
17 */
18
19interface MidiEngineListener {
20 public void onFocusedDeck(int deckIndex);
21}
22
23class MidiEngine {
24
25 private final List<MidiEngineListener> listeners = new ArrayList<MidiEngineListener>();
26 private final List<SCMidiInput> midiControllers = new ArrayList<SCMidiInput>();
27
28 public MidiEngine addListener(MidiEngineListener l) {
29 listeners.add(l);
30 return this;
31 }
32
33 public MidiEngine removeListener(MidiEngineListener l) {
34 listeners.remove(l);
35 return this;
36 }
37
38 private SCMidiInput midiQwertyKeys;
39 private SCMidiInput midiQwertyAPC;
40
41 private int activeDeckIndex = 0;
42
43 public MidiEngine() {
44
45 midiControllers.add(midiQwertyKeys = new SCMidiInput(SCMidiInput.KEYS));
46 midiControllers.add(midiQwertyAPC = new SCMidiInput(SCMidiInput.APC));
47 for (MidiInputDevice device : RWMidi.getInputDevices()) {
48 if (device.getName().contains("APC")) {
49 midiControllers.add(new APC40MidiInput(device).setEnabled(true));
50 } else if (device.getName().contains("SLIDER/KNOB KORG")) {
51 midiControllers.add(new KorgNanoKontrolMidiInput(device).setEnabled(true));
52 } else {
53 midiControllers.add(new SCMidiInput(device));
54 }
55 }
56 }
57
58 public List<SCMidiInput> getControllers() {
59 return this.midiControllers;
60 }
61
62 public MidiEngine setFocusedDeck(int deckIndex) {
63 if (this.activeDeckIndex != deckIndex) {
64 this.activeDeckIndex = deckIndex;
65 for (MidiEngineListener listener : listeners) {
66 listener.onFocusedDeck(deckIndex);
67 }
68 }
69 return this;
70 }
71
72 public Engine.Deck getFocusedDeck() {
73 return lx.engine.getDeck(activeDeckIndex);
74 }
75
76 public boolean isQwertyEnabled() {
77 return midiQwertyKeys.isEnabled() || midiQwertyAPC.isEnabled();
78 }
79}
80
81public interface SCMidiInputListener {
82 public void onEnabled(SCMidiInput controller, boolean enabled);
83}
84
85public class SCMidiInput extends AbstractScrollItem {
86
87 public static final int MIDI = 0;
88 public static final int KEYS = 1;
89 public static final int APC = 2;
90
91 private boolean enabled = false;
92 private final String name;
93 private final int mode;
94 private int octaveShift = 0;
95
96 class NoteMeta {
97 int channel;
98 int number;
99 NoteMeta(int channel, int number) {
100 this.channel = channel;
101 this.number = number;
102 }
103 }
104
105 final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();
106
107 final List<SCMidiInputListener> listeners = new ArrayList<SCMidiInputListener>();
108
109 public SCMidiInput addListener(SCMidiInputListener l) {
110 listeners.add(l);
111 return this;
112 }
113
114 public SCMidiInput removeListener(SCMidiInputListener l) {
115 listeners.remove(l);
116 return this;
117 }
118
119 SCMidiInput(MidiInputDevice d) {
120 mode = MIDI;
121 d.createInput(this);
122 name = d.getName().replace("Unknown vendor","");
123 }
124
125 SCMidiInput(int mode) {
126 this.mode = mode;
127 switch (mode) {
128 case APC:
129 name = "QWERTY (APC Mode)";
130 mapAPC();
131 break;
132 default:
133 case KEYS:
134 name = "QWERTY (Key Mode)";
135 mapKeys();
136 break;
137 }
138 }
139
140 private void mapAPC() {
141 mapNote('1', 0, 53);
142 mapNote('2', 1, 53);
143 mapNote('3', 2, 53);
144 mapNote('4', 3, 53);
145 mapNote('5', 4, 53);
146 mapNote('6', 5, 53);
147 mapNote('q', 0, 54);
148 mapNote('w', 1, 54);
149 mapNote('e', 2, 54);
150 mapNote('r', 3, 54);
151 mapNote('t', 4, 54);
152 mapNote('y', 5, 54);
153 mapNote('a', 0, 55);
154 mapNote('s', 1, 55);
155 mapNote('d', 2, 55);
156 mapNote('f', 3, 55);
157 mapNote('g', 4, 55);
158 mapNote('h', 5, 55);
159 mapNote('z', 0, 56);
160 mapNote('x', 1, 56);
161 mapNote('c', 2, 56);
162 mapNote('v', 3, 56);
163 mapNote('b', 4, 56);
164 mapNote('n', 5, 56);
165 registerKeyEvent(this);
166 }
167
168 private void mapKeys() {
169 int note = 48;
170 mapNote('a', 1, note++);
171 mapNote('w', 1, note++);
172 mapNote('s', 1, note++);
173 mapNote('e', 1, note++);
174 mapNote('d', 1, note++);
175 mapNote('f', 1, note++);
176 mapNote('t', 1, note++);
177 mapNote('g', 1, note++);
178 mapNote('y', 1, note++);
179 mapNote('h', 1, note++);
180 mapNote('u', 1, note++);
181 mapNote('j', 1, note++);
182 mapNote('k', 1, note++);
183 mapNote('o', 1, note++);
184 mapNote('l', 1, note++);
185 registerKeyEvent(this);
186 }
187
188 void mapNote(char ch, int channel, int number) {
189 keyToNote.put(ch, new NoteMeta(channel, number));
190 }
191
192 public String getLabel() {
193 return name;
194 }
195
196 public void keyEvent(KeyEvent e) {
197 if (!enabled) {
198 return;
199 }
200 char c = Character.toLowerCase(e.getKeyChar());
201 NoteMeta nm = keyToNote.get(c);
202 if (nm != null) {
203 switch (e.getID()) {
204 case KeyEvent.KEY_PRESSED:
205 noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
206 break;
207 case KeyEvent.KEY_RELEASED:
208 noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
209 break;
210 }
211 }
212 if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
213 switch (c) {
214 case 'z':
215 octaveShift = constrain(octaveShift-1, -4, 4);
216 break;
217 case 'x':
218 octaveShift = constrain(octaveShift+1, -4, 4);
219 break;
220 }
221 }
222 }
223
224 public boolean isEnabled() {
225 return enabled;
226 }
227
228 public boolean isSelected() {
229 return enabled;
230 }
231
232 public void onMousePressed() {
233 setEnabled(!enabled);
234 }
235
236 public SCMidiInput setEnabled(boolean enabled) {
237 if (enabled != this.enabled) {
238 this.enabled = enabled;
239 for (SCMidiInputListener l : listeners) {
240 l.onEnabled(this, enabled);
241 }
242 }
243 return this;
244 }
245
246 protected SCPattern getFocusedPattern() {
247 return (SCPattern) midiEngine.getFocusedDeck().getActivePattern();
248 }
249
250 private boolean logMidi() {
251 return (uiMidi != null) && uiMidi.logMidi();
252 }
253
254 final void programChangeReceived(ProgramChange pc) {
255 if (!enabled) {
256 return;
257 }
258 if (logMidi()) {
259 println(getLabel() + " :: Program Change :: " + pc.getNumber());
260 }
261 handleProgramChange(pc);
262 }
263
264 final void controllerChangeReceived(rwmidi.Controller cc) {
265 if (!enabled) {
266 return;
267 }
268 if (logMidi()) {
269 println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue());
270 }
271 if (!getFocusedPattern().controllerChangeReceived(cc)) {
272 handleControllerChange(cc);
273 }
274 }
275
276 final void noteOnReceived(Note note) {
277 if (!enabled) {
278 return;
279 }
280 if (logMidi()) {
281 println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
282 }
283 if (!getFocusedPattern().noteOnReceived(note)) {
284 handleNoteOn(note);
285 }
286 }
287
288 final void noteOffReceived(Note note) {
289 if (!enabled) {
290 return;
291 }
292 if (logMidi()) {
293 println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
294 }
295 if (!getFocusedPattern().noteOffReceived(note)) {
296 handleNoteOff(note);
297 }
298 }
299
300 // Subclasses may implement these to map top-level functionality
301 protected void handleProgramChange(ProgramChange pc) {
302 }
303 protected void handleControllerChange(rwmidi.Controller cc) {
304 }
305 protected void handleNoteOn(Note note) {
306 }
307 protected void handleNoteOff(Note note) {
308 }
309}
310
311public class APC40MidiInput extends SCMidiInput {
312
313 private boolean shiftOn = false;
314 private LXEffect releaseEffect = null;
315
316 APC40MidiInput(MidiInputDevice d) {
317 super(d);
318 }
319
320 protected void handleControllerChange(rwmidi.Controller cc) {
321 int number = cc.getCC();
322 switch (number) {
323 // Crossfader
324 case 15:
325 lx.engine.getDeck(1).getCrossfader().setValue(cc.getValue() / 127.);
326 break;
327 }
328
329 int parameterIndex = -1;
330 if (number >= 48 && number <= 55) {
331 parameterIndex = number - 48;
332 } else if (number >= 16 && number <= 19) {
333 parameterIndex = 8 + (number-16);
334 }
335 if (parameterIndex >= 0) {
336 List<LXParameter> parameters = getFocusedPattern().getParameters();
337 if (parameterIndex < parameters.size()) {
338 parameters.get(parameterIndex).setValue(cc.getValue() / 127.);
339 }
340 }
341
342 if (number >= 20 && number <= 23) {
343 int effectIndex = number - 20;
344 List<LXParameter> parameters = glucose.getSelectedEffect().getParameters();
345 if (effectIndex < parameters.size()) {
346 parameters.get(effectIndex).setValue(cc.getValue() / 127.);
347 }
348 }
349 }
350
351 protected void handleNoteOn(Note note) {
352 switch (note.getPitch()) {
353 case 94: // right bank
354 midiEngine.setFocusedDeck(1);
355 break;
356 case 95: // left bank
357 midiEngine.setFocusedDeck(0);
358 break;
359 case 96: // up bank
360 if (shiftOn) {
361 glucose.incrementSelectedEffectBy(1);
362 } else {
363 midiEngine.getFocusedDeck().goNext();
364 }
365 break;
366 case 97: // down bank
367 if (shiftOn) {
368 glucose.incrementSelectedEffectBy(-1);
369 } else {
370 midiEngine.getFocusedDeck().goPrev();
371 }
372 break;
373
374 case 98: // shift
375 shiftOn = true;
376 break;
377
378 case 99: // tap tempo
379 lx.tempo.tap();
380 break;
381 case 100: // nudge+
382 lx.tempo.setBpm(lx.tempo.bpm() + (shiftOn ? 1 : .1));
383 break;
384 case 101: // nudge-
385 lx.tempo.setBpm(lx.tempo.bpm() - (shiftOn ? 1 : .1));
386 break;
387
388 case 91: // play
389 case 93: // rec
390 releaseEffect = glucose.getSelectedEffect();
391 if (releaseEffect.isMomentary()) {
392 releaseEffect.enable();
393 } else {
394 releaseEffect.toggle();
395 }
396 break;
397
398 case 92: // stop
399 glucose.getSelectedEffect().disable();
400 break;
401 }
402 }
403
404 protected void handleNoteOff(Note note) {
405 switch (note.getPitch()) {
406 case 93: // rec
407 if (releaseEffect != null) {
408 if (releaseEffect.isMomentary()) {
409 releaseEffect.disable();
410 }
411 }
412 break;
413
414 case 98: // shift
415 shiftOn = false;
416 break;
417 }
418 }
419}
420
421class KorgNanoKontrolMidiInput extends SCMidiInput {
422
423 KorgNanoKontrolMidiInput(MidiInputDevice d) {
424 super(d);
425 }
426
427 protected void handleControllerChange(rwmidi.Controller cc) {
428 int number = cc.getCC();
429 if (number >= 16 && number <= 23) {
430 int parameterIndex = number - 16;
431 List<LXParameter> parameters = getFocusedPattern().getParameters();
432 if (parameterIndex < parameters.size()) {
433 parameters.get(parameterIndex).setValue(cc.getValue() / 127.);
434 }
435 }
436
437 if (cc.getValue() == 127) {
438 switch (number) {
439 // Left track
440 case 58:
441 midiEngine.setFocusedDeck(0);
442 break;
443 // Right track
444 case 59:
445 midiEngine.setFocusedDeck(1);
446 break;
447 // Left chevron
448 case 43:
449 midiEngine.getFocusedDeck().goPrev();
450 break;
451 // Right chevron
452 case 44:
453 midiEngine.getFocusedDeck().goNext();
454 break;
455 }
456 }
457 }
458}
459