Add APC Output driver so all patterns + FX send their knobs to APC
[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 {
4214e9a2 53 boolean enabled = device.getName().contains("KEYBOARD KORG") || device.getName().contains("Bus 1 Apple");
cc8b3f82 54 midiControllers.add(new SCMidiInput(device).setEnabled(enabled));
d6ac1ee8
MS
55 }
56 }
05965d3d
MS
57 for (MidiOutputDevice device : RWMidi.getOutputDevices()) {
58 if (device.getName().contains("APC")) {
59 new APC40MidiOutput(this, device);
60 }
61 }
d6ac1ee8
MS
62 }
63
64 public List<SCMidiInput> getControllers() {
65 return this.midiControllers;
66 }
67
68 public MidiEngine setFocusedDeck(int deckIndex) {
69 if (this.activeDeckIndex != deckIndex) {
70 this.activeDeckIndex = deckIndex;
71 for (MidiEngineListener listener : listeners) {
72 listener.onFocusedDeck(deckIndex);
73 }
74 }
75 return this;
76 }
77
78 public Engine.Deck getFocusedDeck() {
79 return lx.engine.getDeck(activeDeckIndex);
80 }
81
82 public boolean isQwertyEnabled() {
83 return midiQwertyKeys.isEnabled() || midiQwertyAPC.isEnabled();
84 }
85}
86
87public interface SCMidiInputListener {
88 public void onEnabled(SCMidiInput controller, boolean enabled);
89}
90
91public class SCMidiInput extends AbstractScrollItem {
92
93 public static final int MIDI = 0;
94 public static final int KEYS = 1;
95 public static final int APC = 2;
96
97 private boolean enabled = false;
98 private final String name;
99 private final int mode;
100 private int octaveShift = 0;
101
102 class NoteMeta {
103 int channel;
104 int number;
105 NoteMeta(int channel, int number) {
106 this.channel = channel;
107 this.number = number;
108 }
109 }
110
111 final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();
112
113 final List<SCMidiInputListener> listeners = new ArrayList<SCMidiInputListener>();
114
115 public SCMidiInput addListener(SCMidiInputListener l) {
116 listeners.add(l);
117 return this;
118 }
119
120 public SCMidiInput removeListener(SCMidiInputListener l) {
121 listeners.remove(l);
122 return this;
123 }
124
125 SCMidiInput(MidiInputDevice d) {
126 mode = MIDI;
127 d.createInput(this);
128 name = d.getName().replace("Unknown vendor","");
129 }
130
131 SCMidiInput(int mode) {
132 this.mode = mode;
133 switch (mode) {
134 case APC:
135 name = "QWERTY (APC Mode)";
136 mapAPC();
137 break;
138 default:
139 case KEYS:
140 name = "QWERTY (Key Mode)";
141 mapKeys();
142 break;
143 }
144 }
145
146 private void mapAPC() {
147 mapNote('1', 0, 53);
148 mapNote('2', 1, 53);
149 mapNote('3', 2, 53);
150 mapNote('4', 3, 53);
151 mapNote('5', 4, 53);
152 mapNote('6', 5, 53);
153 mapNote('q', 0, 54);
154 mapNote('w', 1, 54);
155 mapNote('e', 2, 54);
156 mapNote('r', 3, 54);
157 mapNote('t', 4, 54);
158 mapNote('y', 5, 54);
159 mapNote('a', 0, 55);
160 mapNote('s', 1, 55);
161 mapNote('d', 2, 55);
162 mapNote('f', 3, 55);
163 mapNote('g', 4, 55);
164 mapNote('h', 5, 55);
165 mapNote('z', 0, 56);
166 mapNote('x', 1, 56);
167 mapNote('c', 2, 56);
168 mapNote('v', 3, 56);
169 mapNote('b', 4, 56);
170 mapNote('n', 5, 56);
171 registerKeyEvent(this);
172 }
173
174 private void mapKeys() {
175 int note = 48;
176 mapNote('a', 1, note++);
177 mapNote('w', 1, note++);
178 mapNote('s', 1, note++);
179 mapNote('e', 1, note++);
180 mapNote('d', 1, note++);
181 mapNote('f', 1, note++);
182 mapNote('t', 1, note++);
183 mapNote('g', 1, note++);
184 mapNote('y', 1, note++);
185 mapNote('h', 1, note++);
186 mapNote('u', 1, note++);
187 mapNote('j', 1, note++);
188 mapNote('k', 1, note++);
189 mapNote('o', 1, note++);
190 mapNote('l', 1, note++);
191 registerKeyEvent(this);
192 }
193
194 void mapNote(char ch, int channel, int number) {
195 keyToNote.put(ch, new NoteMeta(channel, number));
196 }
197
198 public String getLabel() {
199 return name;
200 }
201
202 public void keyEvent(KeyEvent e) {
203 if (!enabled) {
204 return;
205 }
206 char c = Character.toLowerCase(e.getKeyChar());
207 NoteMeta nm = keyToNote.get(c);
208 if (nm != null) {
209 switch (e.getID()) {
210 case KeyEvent.KEY_PRESSED:
211 noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
212 break;
213 case KeyEvent.KEY_RELEASED:
214 noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
215 break;
216 }
217 }
218 if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
219 switch (c) {
220 case 'z':
221 octaveShift = constrain(octaveShift-1, -4, 4);
222 break;
223 case 'x':
224 octaveShift = constrain(octaveShift+1, -4, 4);
225 break;
226 }
227 }
228 }
229
230 public boolean isEnabled() {
231 return enabled;
232 }
233
234 public boolean isSelected() {
235 return enabled;
236 }
237
238 public void onMousePressed() {
239 setEnabled(!enabled);
240 }
241
242 public SCMidiInput setEnabled(boolean enabled) {
243 if (enabled != this.enabled) {
244 this.enabled = enabled;
245 for (SCMidiInputListener l : listeners) {
246 l.onEnabled(this, enabled);
247 }
248 }
249 return this;
250 }
251
252 protected SCPattern getFocusedPattern() {
253 return (SCPattern) midiEngine.getFocusedDeck().getActivePattern();
254 }
255
256 private boolean logMidi() {
257 return (uiMidi != null) && uiMidi.logMidi();
258 }
259
260 final void programChangeReceived(ProgramChange pc) {
261 if (!enabled) {
262 return;
263 }
264 if (logMidi()) {
265 println(getLabel() + " :: Program Change :: " + pc.getNumber());
266 }
267 handleProgramChange(pc);
268 }
269
270 final void controllerChangeReceived(rwmidi.Controller cc) {
271 if (!enabled) {
272 return;
273 }
274 if (logMidi()) {
275 println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue());
276 }
277 if (!getFocusedPattern().controllerChangeReceived(cc)) {
278 handleControllerChange(cc);
279 }
280 }
281
282 final void noteOnReceived(Note note) {
283 if (!enabled) {
284 return;
285 }
286 if (logMidi()) {
287 println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
288 }
289 if (!getFocusedPattern().noteOnReceived(note)) {
290 handleNoteOn(note);
291 }
292 }
293
294 final void noteOffReceived(Note note) {
295 if (!enabled) {
296 return;
297 }
298 if (logMidi()) {
299 println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
300 }
301 if (!getFocusedPattern().noteOffReceived(note)) {
302 handleNoteOff(note);
303 }
304 }
305
306 // Subclasses may implement these to map top-level functionality
307 protected void handleProgramChange(ProgramChange pc) {
308 }
309 protected void handleControllerChange(rwmidi.Controller cc) {
310 }
311 protected void handleNoteOn(Note note) {
312 }
313 protected void handleNoteOff(Note note) {
314 }
315}
316
317public class APC40MidiInput extends SCMidiInput {
318
319 private boolean shiftOn = false;
320 private LXEffect releaseEffect = null;
321
322 APC40MidiInput(MidiInputDevice d) {
323 super(d);
324 }
325
326 protected void handleControllerChange(rwmidi.Controller cc) {
327 int number = cc.getCC();
328 switch (number) {
329 // Crossfader
330 case 15:
331 lx.engine.getDeck(1).getCrossfader().setValue(cc.getValue() / 127.);
332 break;
333 }
334
335 int parameterIndex = -1;
336 if (number >= 48 && number <= 55) {
337 parameterIndex = number - 48;
338 } else if (number >= 16 && number <= 19) {
339 parameterIndex = 8 + (number-16);
340 }
341 if (parameterIndex >= 0) {
342 List<LXParameter> parameters = getFocusedPattern().getParameters();
343 if (parameterIndex < parameters.size()) {
344 parameters.get(parameterIndex).setValue(cc.getValue() / 127.);
345 }
346 }
347
348 if (number >= 20 && number <= 23) {
349 int effectIndex = number - 20;
350 List<LXParameter> parameters = glucose.getSelectedEffect().getParameters();
351 if (effectIndex < parameters.size()) {
352 parameters.get(effectIndex).setValue(cc.getValue() / 127.);
353 }
354 }
355 }
356
05965d3d 357 private long tap1 = 0;
3661fcee 358
05965d3d
MS
359 private boolean lbtwn(long a, long b, long c) {
360 return a >= b && a <= c;
361 }
3661fcee 362
d6ac1ee8 363 protected void handleNoteOn(Note note) {
05965d3d 364 int nPitch = note.getPitch(), nChan = note.getChannel();
3661fcee 365 switch (nPitch) {
366
05965d3d
MS
367 case 82: // scene 1
368 EFF_boom.trigger();
369 break;
370
371 case 83: // scene 2
372 EFF_flash.trigger();
373 break;
374
375 case 90:
376 // dan's dirty tapping mechanism
377 lx.tempo.trigger();
378 tap1 = millis();
379 break;
af83f61c 380
381 case 91: // play
382 case 95: // bank
383 midiEngine.setFocusedDeck(0);
384 break;
385
386 case 93: // rec
d6ac1ee8
MS
387 case 94: // right bank
388 midiEngine.setFocusedDeck(1);
389 break;
af83f61c 390
d6ac1ee8
MS
391 case 96: // up bank
392 if (shiftOn) {
393 glucose.incrementSelectedEffectBy(1);
394 } else {
395 midiEngine.getFocusedDeck().goNext();
396 }
397 break;
398 case 97: // down bank
399 if (shiftOn) {
400 glucose.incrementSelectedEffectBy(-1);
401 } else {
402 midiEngine.getFocusedDeck().goPrev();
403 }
404 break;
405
406 case 98: // shift
407 shiftOn = true;
408 break;
409
410 case 99: // tap tempo
411 lx.tempo.tap();
412 break;
413 case 100: // nudge+
414 lx.tempo.setBpm(lx.tempo.bpm() + (shiftOn ? 1 : .1));
415 break;
416 case 101: // nudge-
417 lx.tempo.setBpm(lx.tempo.bpm() - (shiftOn ? 1 : .1));
418 break;
419
af83f61c 420 case 62: // Detail View
d6ac1ee8
MS
421 releaseEffect = glucose.getSelectedEffect();
422 if (releaseEffect.isMomentary()) {
423 releaseEffect.enable();
424 } else {
425 releaseEffect.toggle();
426 }
427 break;
428
af83f61c 429 case 63: // rec quantize
d6ac1ee8
MS
430 glucose.getSelectedEffect().disable();
431 break;
432 }
433 }
434
435 protected void handleNoteOff(Note note) {
05965d3d 436 int nPitch = note.getPitch(), nChan = note.getChannel();
3661fcee 437 switch (nPitch) {
05965d3d
MS
438 case 90:
439 long tapDelta = millis() - tap1;
440 if (lbtwn(tapDelta,5000,300*1000)) { // hackish tapping mechanism
441 double bpm = 32.*60000./(tapDelta);
442 while (bpm < 20) bpm*=2;
443 while (bpm > 40) bpm/=2;
444 lx.tempo.setBpm(bpm);
445 lx.tempo.trigger();
446 tap1 = 0;
447 println("Tap Set - " + bpm + " bpm");
448 }
449 break;
af83f61c 450
451 case 63: // rec quantize
d6ac1ee8
MS
452 if (releaseEffect != null) {
453 if (releaseEffect.isMomentary()) {
454 releaseEffect.disable();
455 }
456 }
457 break;
af83f61c 458
d6ac1ee8
MS
459 case 98: // shift
460 shiftOn = false;
05965d3d 461 break;
d6ac1ee8
MS
462 }
463 }
464}
465
466class KorgNanoKontrolMidiInput extends SCMidiInput {
467
468 KorgNanoKontrolMidiInput(MidiInputDevice d) {
469 super(d);
470 }
471
472 protected void handleControllerChange(rwmidi.Controller cc) {
473 int number = cc.getCC();
474 if (number >= 16 && number <= 23) {
475 int parameterIndex = number - 16;
476 List<LXParameter> parameters = getFocusedPattern().getParameters();
477 if (parameterIndex < parameters.size()) {
478 parameters.get(parameterIndex).setValue(cc.getValue() / 127.);
479 }
480 }
481
482 if (cc.getValue() == 127) {
483 switch (number) {
484 // Left track
485 case 58:
486 midiEngine.setFocusedDeck(0);
487 break;
488 // Right track
489 case 59:
490 midiEngine.setFocusedDeck(1);
491 break;
492 // Left chevron
493 case 43:
494 midiEngine.getFocusedDeck().goPrev();
495 break;
496 // Right chevron
497 case 44:
498 midiEngine.getFocusedDeck().goNext();
499 break;
500 }
501 }
502 }
503}
504
05965d3d
MS
505class APC40MidiOutput implements LXParameter.Listener {
506
507 private final MidiEngine midiEngine;
508 private final MidiOutput output;
509 private LXPattern focusedPattern = null;
510 private LXEffect focusedEffect = null;
511
512 APC40MidiOutput(MidiEngine midiEngine, MidiOutputDevice device) {
513 this.midiEngine = midiEngine;
514 output = device.createOutput();
515 midiEngine.addListener(new MidiEngineListener() {
516 public void onFocusedDeck(int deckIndex) {
517 resetPatternParameters();
518 }
519 });
520 glucose.addEffectListener(new GLucose.EffectListener() {
521 public void effectSelected(LXEffect effect) {
522 resetEffectParameters();
523 }
524 });
525 Engine.Listener deckListener = new Engine.AbstractListener() {
526 public void patternDidChange(Engine.Deck deck, LXPattern pattern) {
527 resetPatternParameters();
528 }
529 };
530 for (Engine.Deck d : lx.engine.getDecks()) {
531 d.addListener(deckListener);
532 }
533 resetParameters();
534 }
535
536 private void resetParameters() {
537 resetPatternParameters();
538 resetEffectParameters();
539 }
540
541 private void resetPatternParameters() {
542 LXPattern newPattern = midiEngine.getFocusedDeck().getActivePattern();
543 if (newPattern == focusedPattern) {
544 return;
545 }
546 if (focusedPattern != null) {
547 for (LXParameter p : focusedPattern.getParameters()) {
548 ((LXListenableParameter) p).removeListener(this);
549 }
550 }
551 focusedPattern = newPattern;
552 int i = 0;
553 for (LXParameter p : focusedPattern.getParameters()) {
554 ((LXListenableParameter) p).addListener(this);
555 sendKnob(i++, p);
556 }
557 while (i < 12) {
558 sendKnob(i++, 0);
559 }
560 }
561
562 private void resetEffectParameters() {
563 LXEffect newEffect = glucose.getSelectedEffect();
564 if (newEffect == focusedEffect) {
565 return;
566 }
567 if (focusedEffect != null) {
568 for (LXParameter p : focusedPattern.getParameters()) {
569 ((LXListenableParameter) p).removeListener(this);
570 }
571 }
572 focusedEffect = newEffect;
573 int i = 0;
574 for (LXParameter p : focusedEffect.getParameters()) {
575 ((LXListenableParameter) p).addListener(this);
576 sendKnob(12 + i++, p);
577 }
578 while (i < 4) {
579 sendKnob(12 + i++, 0);
580 }
581 }
582
583 private void sendKnob(int i, LXParameter p) {
584 sendKnob(i, (int) (p.getValuef() * 127.));
585 }
586
587 private void sendKnob(int i, int value) {
588 if (i < 8) {
589 output.sendController(0, 48+i, value);
590 } else if (i < 16) {
591 output.sendController(0, 8+i, value);
592 }
593 }
594
595 public void onParameterChanged(LXParameter parameter) {
596 int i = 0;
597 for (LXParameter p : focusedPattern.getParameters()) {
598 if (p == parameter) {
599 sendKnob(i, p);
600 break;
601 }
602 ++i;
603 }
604 i = 12;
605 for (LXParameter p : focusedEffect.getParameters()) {
606 if (p == parameter) {
607 sendKnob(i, p);
608 break;
609 }
610 ++i;
611 }
612 }
613}