Add two virtual keyboard modes
[SugarCubes.git] / _Internals.pde
1 /**
2 * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND
3 *
4 * //\\ //\\ //\\ //\\
5 * ///\\\ ///\\\ ///\\\ ///\\\
6 * \\\/// \\\/// \\\/// \\\///
7 * \\// \\// \\// \\//
8 *
9 * EXPERTS ONLY!! EXPERTS ONLY!!
10 *
11 * If you are an artist, you may ignore this file! It just sets
12 * up the framework to run the patterns. Should not need modification
13 * for general animation work.
14 */
15
16 import glucose.*;
17 import glucose.control.*;
18 import glucose.effect.*;
19 import glucose.model.*;
20 import glucose.pattern.*;
21 import glucose.transform.*;
22 import glucose.transition.*;
23 import heronarts.lx.*;
24 import heronarts.lx.control.*;
25 import heronarts.lx.effect.*;
26 import heronarts.lx.modulator.*;
27 import heronarts.lx.pattern.*;
28 import heronarts.lx.transition.*;
29 import ddf.minim.*;
30 import ddf.minim.analysis.*;
31 import processing.opengl.*;
32 import rwmidi.*;
33
34 final int VIEWPORT_WIDTH = 900;
35 final int VIEWPORT_HEIGHT = 700;
36
37 // The trailer is measured from the outside of the black metal (but not including the higher welded part on the front)
38 final float TRAILER_WIDTH = 240;
39 final float TRAILER_DEPTH = 97;
40 final float TRAILER_HEIGHT = 33;
41
42 int targetFramerate = 60;
43 int startMillis, lastMillis;
44
45 // Core engine variables
46 GLucose glucose;
47 HeronLX lx;
48 LXPattern[] patterns;
49 MappingTool mappingTool;
50 PandaDriver[] pandaBoards;
51 MidiListener midiQwertyKeys;
52 MidiListener midiQwertyAPC;
53
54 // Display configuration mode
55 boolean mappingMode = false;
56 boolean debugMode = false;
57 DebugUI debugUI;
58 boolean uiOn = true;
59 LXPattern restoreToPattern = null;
60
61 // Handles to UI objects
62 UIContext[] overlays;
63 UIPatternDeck uiPatternA;
64 UICrossfader uiCrossfader;
65 UIMidi uiMidi;
66 UIMapping uiMapping;
67 UIDebugText uiDebugText;
68
69 // Camera variables
70 float eyeR, eyeA, eyeX, eyeY, eyeZ, midX, midY, midZ;
71
72 /**
73 * Engine construction and initialization.
74 */
75 LXPattern[] _patterns(GLucose glucose) {
76 LXPattern[] patterns = patterns(glucose);
77 for (LXPattern p : patterns) {
78 p.setTransition(new DissolveTransition(glucose.lx).setDuration(1000));
79 }
80 return patterns;
81 }
82
83 void logTime(String evt) {
84 int now = millis();
85 println(evt + ": " + (now - lastMillis) + "ms");
86 lastMillis = now;
87 }
88
89 void setup() {
90 startMillis = lastMillis = millis();
91
92 // Initialize the Processing graphics environment
93 size(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, OPENGL);
94 frameRate(targetFramerate);
95 noSmooth();
96 // hint(ENABLE_OPENGL_4X_SMOOTH); // no discernable improvement?
97 logTime("Created viewport");
98
99 // Create the GLucose engine to run the cubes
100 glucose = new GLucose(this, buildModel());
101 lx = glucose.lx;
102 lx.enableKeyboardTempo();
103 logTime("Built GLucose engine");
104
105 // Set the patterns
106 Engine engine = lx.engine;
107 engine.setPatterns(patterns = _patterns(glucose));
108 engine.addDeck(_patterns(glucose));
109 logTime("Built patterns");
110 glucose.setTransitions(transitions(glucose));
111 logTime("Built transitions");
112 glucose.lx.addEffects(effects(glucose));
113 logTime("Built effects");
114
115 // Build output driver
116 PandaMapping[] pandaMappings = buildPandaList();
117 pandaBoards = new PandaDriver[pandaMappings.length];
118 int pbi = 0;
119 for (PandaMapping pm : pandaMappings) {
120 pandaBoards[pbi++] = new PandaDriver(pm.ip, glucose.model, pm);
121 }
122 mappingTool = new MappingTool(glucose, pandaMappings);
123 logTime("Built PandaDriver");
124
125 // MIDI devices
126 List<MidiListener> midiListeners = new ArrayList<MidiListener>();
127 midiListeners.add(midiQwertyKeys = new MidiListener(MidiListener.KEYS));
128 midiListeners.add(midiQwertyAPC = new MidiListener(MidiListener.APC));
129 for (MidiInputDevice device : RWMidi.getInputDevices()) {
130 boolean enableDevice = device.getName().contains("APC");
131 midiListeners.add(new MidiListener(device).setEnabled(enableDevice));
132 }
133 SCMidiDevices.initializeStandardDevices(glucose);
134 logTime("Setup MIDI devices");
135
136 // Build overlay UI
137 debugUI = new DebugUI(pandaMappings);
138 overlays = new UIContext[] {
139 uiPatternA = new UIPatternDeck(lx.engine.getDeck(0), "PATTERN A", 4, 4, 140, 324),
140 new UIBlendMode(4, 332, 140, 86),
141 new UIEffects(4, 422, 140, 144),
142 new UITempo(4, 570, 140, 50),
143 new UISpeed(4, 624, 140, 50),
144
145 new UIPatternDeck(lx.engine.getDeck(1), "PATTERN B", width-144, 4, 140, 324),
146 uiMidi = new UIMidi(midiListeners, width-144, 332, 140, 158),
147 new UIOutput(width-144, 494, 140, 106),
148
149 uiCrossfader = new UICrossfader(width/2-90, height-90, 180, 86),
150
151 uiDebugText = new UIDebugText(148, height-138, width-304, 44),
152 uiMapping = new UIMapping(mappingTool, 4, 4, 140, 324),
153 };
154 uiMapping.setVisible(false);
155 logTime("Built overlay UI");
156
157 // Setup camera
158 midX = TRAILER_WIDTH/2.;
159 midY = glucose.model.yMax/2;
160 midZ = TRAILER_DEPTH/2.;
161 eyeR = -290;
162 eyeA = .15;
163 eyeY = midY + 70;
164 eyeX = midX + eyeR*sin(eyeA);
165 eyeZ = midZ + eyeR*cos(eyeA);
166 addMouseWheelListener(new java.awt.event.MouseWheelListener() {
167 public void mouseWheelMoved(java.awt.event.MouseWheelEvent mwe) {
168 mouseWheel(mwe.getWheelRotation());
169 }});
170
171 println("Total setup: " + (millis() - startMillis) + "ms");
172 println("Hit the 'p' key to toggle Panda Board output");
173 }
174
175 public class MidiListener extends AbstractScrollItem {
176
177 public static final int MIDI = 0;
178 public static final int KEYS = 1;
179 public static final int APC = 2;
180
181 private boolean enabled = false;
182 private final String name;
183
184 MidiListener(MidiInputDevice d) {
185 mode = MIDI;
186 d.createInput(this);
187 name = d.getName();
188 }
189
190 class NoteMeta {
191 int channel;
192 int number;
193 NoteMeta(int channel, int number) {
194 this.channel = channel;
195 this.number = number;
196 }
197 }
198
199 final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();
200
201 private final int mode;
202 private int octaveShift = 0;
203
204 MidiListener(int mode) {
205 this.mode = mode;
206 switch (mode) {
207 case APC:
208 name = "QWERTY (APC Mode)";
209 mapAPC();
210 break;
211 default:
212 case KEYS:
213 name = "QWERTY (Key Mode)";
214 mapKeys();
215 break;
216 }
217 }
218
219 private void mapAPC() {
220 mapNote('1', 0, 53);
221 mapNote('2', 1, 53);
222 mapNote('3', 2, 53);
223 mapNote('4', 3, 53);
224 mapNote('5', 4, 53);
225 mapNote('6', 5, 53);
226 mapNote('q', 0, 54);
227 mapNote('w', 1, 54);
228 mapNote('e', 2, 54);
229 mapNote('r', 3, 54);
230 mapNote('t', 4, 54);
231 mapNote('y', 5, 54);
232 mapNote('a', 0, 55);
233 mapNote('s', 1, 55);
234 mapNote('d', 2, 55);
235 mapNote('f', 3, 55);
236 mapNote('g', 4, 55);
237 mapNote('h', 5, 55);
238 mapNote('z', 0, 56);
239 mapNote('x', 1, 56);
240 mapNote('c', 2, 56);
241 mapNote('v', 3, 56);
242 mapNote('b', 4, 56);
243 mapNote('n', 5, 56);
244 registerKeyEvent(this);
245 }
246
247 private void mapKeys() {
248 int note = 48;
249 mapNote('a', 1, note++);
250 mapNote('w', 1, note++);
251 mapNote('s', 1, note++);
252 mapNote('e', 1, note++);
253 mapNote('d', 1, note++);
254 mapNote('f', 1, note++);
255 mapNote('t', 1, note++);
256 mapNote('g', 1, note++);
257 mapNote('y', 1, note++);
258 mapNote('h', 1, note++);
259 mapNote('u', 1, note++);
260 mapNote('j', 1, note++);
261 mapNote('k', 1, note++);
262 mapNote('o', 1, note++);
263 mapNote('l', 1, note++);
264 registerKeyEvent(this);
265 }
266
267 void mapNote(char ch, int channel, int number) {
268 keyToNote.put(ch, new NoteMeta(channel, number));
269 }
270
271 public String getLabel() {
272 return name;
273 }
274
275 public void keyEvent(KeyEvent e) {
276 if (!enabled) {
277 return;
278 }
279 char c = Character.toLowerCase(e.getKeyChar());
280 NoteMeta nm = keyToNote.get(c);
281 if (nm != null) {
282 switch (e.getID()) {
283 case KeyEvent.KEY_PRESSED:
284 noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
285 break;
286 case KeyEvent.KEY_RELEASED:
287 noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
288 break;
289 }
290 }
291 if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
292 switch (c) {
293 case 'z':
294 octaveShift = constrain(octaveShift-1, -4, 4);
295 break;
296 case 'x':
297 octaveShift = constrain(octaveShift+1, -4, 4);
298 break;
299 }
300 }
301 }
302
303 public boolean isEnabled() {
304 return enabled;
305 }
306
307 public boolean isSelected() {
308 return enabled;
309 }
310
311 public void onMousePressed() {
312 setEnabled(!enabled);
313 }
314
315 public MidiListener setEnabled(boolean enabled) {
316 if (enabled != this.enabled) {
317 this.enabled = enabled;
318 uiMidi.redraw();
319 }
320 return this;
321 }
322
323 private SCPattern getFocusedPattern() {
324 return (SCPattern) uiMidi.getFocusedDeck().getActivePattern();
325 }
326
327 void programChangeReceived(ProgramChange pc) {
328 if (!enabled) {
329 return;
330 }
331 if (uiMidi.logMidi()) {
332 println(getLabel() + " :: Program Change :: " + pc.getNumber());
333 }
334 }
335
336 void controllerChangeReceived(rwmidi.Controller cc) {
337 if (!enabled) {
338 return;
339 }
340 if (uiMidi.logMidi()) {
341 println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue());
342 }
343 getFocusedPattern().controllerChangeReceived(cc);
344 }
345
346 void noteOnReceived(Note note) {
347 if (!enabled) {
348 return;
349 }
350 if (uiMidi.logMidi()) {
351 println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
352 }
353 getFocusedPattern().noteOnReceived(note);
354 }
355
356 void noteOffReceived(Note note) {
357 if (!enabled) {
358 return;
359 }
360 if (uiMidi.logMidi()) {
361 println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
362 }
363 getFocusedPattern().noteOffReceived(note);
364 }
365
366 }
367
368 /**
369 * Core render loop and drawing functionality.
370 */
371 void draw() {
372 // Draws the simulation and the 2D UI overlay
373 background(40);
374 color[] colors = glucose.getColors();
375
376 String displayMode = uiCrossfader.getDisplayMode();
377 if (displayMode == "A") {
378 colors = lx.engine.getDeck(0).getColors();
379 } else if (displayMode == "B") {
380 colors = lx.engine.getDeck(1).getColors();
381 }
382 if (debugMode) {
383 debugUI.maskColors(colors);
384 }
385
386 camera(
387 eyeX, eyeY, eyeZ,
388 midX, midY, midZ,
389 0, -1, 0
390 );
391
392 translate(0, 40, 0);
393
394 noStroke();
395 fill(#141414);
396 drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
397 fill(#070707);
398 stroke(#222222);
399 beginShape();
400 vertex(0, 0, 0);
401 vertex(TRAILER_WIDTH, 0, 0);
402 vertex(TRAILER_WIDTH, 0, TRAILER_DEPTH);
403 vertex(0, 0, TRAILER_DEPTH);
404 endShape();
405
406 noStroke();
407 // drawBassBox(glucose.model.bassBox);
408 // for (Speaker s : glucose.model.speakers) {
409 // drawSpeaker(s);
410 // }
411 for (Cube c : glucose.model.cubes) {
412 drawCube(c);
413 }
414
415 noFill();
416 strokeWeight(2);
417 beginShape(POINTS);
418 // TODO(mcslee): restore when bassBox/speakers are right again
419 // for (Point p : glucose.model.points) {
420 for (Cube cube : glucose.model.cubes) {
421 for (Point p : cube.points) {
422 stroke(colors[p.index]);
423 vertex(p.fx, p.fy, p.fz);
424 }
425 }
426 endShape();
427
428 // 2D Overlay UI
429 drawUI();
430
431 // Send output colors
432 color[] sendColors = glucose.getColors();
433 if (debugMode) {
434 debugUI.maskColors(colors);
435 }
436
437 // Gamma correction here. Apply a cubic to the brightness
438 // for better representation of dynamic range
439 for (int i = 0; i < colors.length; ++i) {
440 float b = brightness(colors[i]) / 100.f;
441 colors[i] = color(
442 hue(colors[i]),
443 saturation(colors[i]),
444 (b*b*b) * 100.
445 );
446 }
447
448 // TODO(mcslee): move into GLucose engine
449 for (PandaDriver p : pandaBoards) {
450 p.send(colors);
451 }
452 }
453
454 void drawBassBox(BassBox b) {
455 float in = .15;
456
457 noStroke();
458 fill(#191919);
459 pushMatrix();
460 translate(b.x + BassBox.EDGE_WIDTH/2., b.y + BassBox.EDGE_HEIGHT/2, b.z + BassBox.EDGE_DEPTH/2.);
461 box(BassBox.EDGE_WIDTH-20*in, BassBox.EDGE_HEIGHT-20*in, BassBox.EDGE_DEPTH-20*in);
462 popMatrix();
463
464 noStroke();
465 fill(#393939);
466 drawBox(b.x+in, b.y+in, b.z+in, 0, 0, 0, BassBox.EDGE_WIDTH-in*2, BassBox.EDGE_HEIGHT-in*2, BassBox.EDGE_DEPTH-in*2, Cube.CHANNEL_WIDTH-in);
467
468 pushMatrix();
469 translate(b.x+(Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT-in, b.z + BassBox.EDGE_DEPTH/2.);
470 float lastOffset = 0;
471 for (float offset : BoothFloor.STRIP_OFFSETS) {
472 translate(offset - lastOffset, 0, 0);
473 box(Cube.CHANNEL_WIDTH-in, 0, BassBox.EDGE_DEPTH - 2*in);
474 lastOffset = offset;
475 }
476 popMatrix();
477
478 pushMatrix();
479 translate(b.x + (Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT/2., b.z + in);
480 for (int j = 0; j < 2; ++j) {
481 pushMatrix();
482 for (int i = 0; i < BassBox.NUM_FRONT_STRUTS; ++i) {
483 translate(BassBox.FRONT_STRUT_SPACING, 0, 0);
484 box(Cube.CHANNEL_WIDTH-in, BassBox.EDGE_HEIGHT - in*2, 0);
485 }
486 popMatrix();
487 translate(0, 0, BassBox.EDGE_DEPTH - 2*in);
488 }
489 popMatrix();
490
491 pushMatrix();
492 translate(b.x + in, b.y + BassBox.EDGE_HEIGHT/2., b.z + BassBox.SIDE_STRUT_SPACING + (Cube.CHANNEL_WIDTH-in)/2.);
493 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
494 translate(BassBox.EDGE_WIDTH-2*in, 0, 0);
495 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
496 popMatrix();
497
498 }
499
500 void drawCube(Cube c) {
501 float in = .15;
502 noStroke();
503 fill(#393939);
504 drawBox(c.x+in, c.y+in, c.z+in, c.rx, c.ry, c.rz, Cube.EDGE_WIDTH-in*2, Cube.EDGE_HEIGHT-in*2, Cube.EDGE_WIDTH-in*2, Cube.CHANNEL_WIDTH-in);
505 }
506
507 void drawSpeaker(Speaker s) {
508 float in = .15;
509
510 noStroke();
511 fill(#191919);
512 pushMatrix();
513 translate(s.x, s.y, s.z);
514 rotate(s.ry / 180. * PI, 0, -1, 0);
515 translate(Speaker.EDGE_WIDTH/2., Speaker.EDGE_HEIGHT/2., Speaker.EDGE_DEPTH/2.);
516 box(Speaker.EDGE_WIDTH-20*in, Speaker.EDGE_HEIGHT-20*in, Speaker.EDGE_DEPTH-20*in);
517 translate(0, Speaker.EDGE_HEIGHT/2. + Speaker.EDGE_HEIGHT*.8/2, 0);
518
519 fill(#222222);
520 box(Speaker.EDGE_WIDTH*.6, Speaker.EDGE_HEIGHT*.8, Speaker.EDGE_DEPTH*.75);
521 popMatrix();
522
523 noStroke();
524 fill(#393939);
525 drawBox(s.x+in, s.y+in, s.z+in, 0, s.ry, 0, Speaker.EDGE_WIDTH-in*2, Speaker.EDGE_HEIGHT-in*2, Speaker.EDGE_DEPTH-in*2, Cube.CHANNEL_WIDTH-in);
526 }
527
528 void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, float yd, float zd, float sw) {
529 pushMatrix();
530 translate(x, y, z);
531 rotate(rx / 180. * PI, -1, 0, 0);
532 rotate(ry / 180. * PI, 0, -1, 0);
533 rotate(rz / 180. * PI, 0, 0, -1);
534 for (int i = 0; i < 4; ++i) {
535 float wid = (i % 2 == 0) ? xd : zd;
536
537 beginShape();
538 vertex(0, 0);
539 vertex(wid, 0);
540 vertex(wid, yd);
541 vertex(wid - sw, yd);
542 vertex(wid - sw, sw);
543 vertex(0, sw);
544 endShape();
545 beginShape();
546 vertex(0, sw);
547 vertex(0, yd);
548 vertex(wid - sw, yd);
549 vertex(wid - sw, yd - sw);
550 vertex(sw, yd - sw);
551 vertex(sw, sw);
552 endShape();
553
554 translate(wid, 0, 0);
555 rotate(HALF_PI, 0, -1, 0);
556 }
557 popMatrix();
558 }
559
560 void drawUI() {
561 camera();
562 javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
563 gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
564 ((PGraphicsOpenGL)g).endGL();
565 strokeWeight(1);
566
567 if (uiOn) {
568 for (UIContext context : overlays) {
569 context.draw();
570 }
571 }
572
573 // Always draw FPS meter
574 fill(#555555);
575 textSize(9);
576 textAlign(LEFT, BASELINE);
577 text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
578
579 if (debugMode) {
580 debugUI.draw();
581 }
582 }
583
584 /**
585 * Top-level keyboard event handling
586 */
587 void keyPressed() {
588 if (mappingMode) {
589 mappingTool.keyPressed(uiMapping);
590 }
591 switch (key) {
592 case '-':
593 case '_':
594 frameRate(--targetFramerate);
595 break;
596 case '=':
597 case '+':
598 frameRate(++targetFramerate);
599 break;
600 case 'd':
601 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
602 debugMode = !debugMode;
603 println("Debug output: " + (debugMode ? "ON" : "OFF"));
604 }
605 break;
606 case 'm':
607 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
608 mappingMode = !mappingMode;
609 uiPatternA.setVisible(!mappingMode);
610 uiMapping.setVisible(mappingMode);
611 if (mappingMode) {
612 restoreToPattern = lx.getPattern();
613 lx.setPatterns(new LXPattern[] { mappingTool });
614 } else {
615 lx.setPatterns(patterns);
616 LXTransition pop = restoreToPattern.getTransition();
617 restoreToPattern.setTransition(null);
618 lx.goPattern(restoreToPattern);
619 restoreToPattern.setTransition(pop);
620 }
621 }
622 break;
623 case 'p':
624 for (PandaDriver p : pandaBoards) {
625 p.toggle();
626 }
627 break;
628 case 'u':
629 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
630 uiOn = !uiOn;
631 }
632 break;
633 }
634 }
635
636 /**
637 * Top-level mouse event handling
638 */
639 int mx, my;
640 void mousePressed() {
641 boolean debugged = false;
642 if (debugMode) {
643 debugged = debugUI.mousePressed();
644 }
645 if (!debugged) {
646 for (UIContext context : overlays) {
647 context.mousePressed(mouseX, mouseY);
648 }
649 }
650 mx = mouseX;
651 my = mouseY;
652 }
653
654 void mouseDragged() {
655 boolean dragged = false;
656 for (UIContext context : overlays) {
657 dragged |= context.mouseDragged(mouseX, mouseY);
658 }
659 if (!dragged) {
660 int dx = mouseX - mx;
661 int dy = mouseY - my;
662 mx = mouseX;
663 my = mouseY;
664 eyeA += dx*.003;
665 eyeX = midX + eyeR*sin(eyeA);
666 eyeZ = midZ + eyeR*cos(eyeA);
667 eyeY += dy;
668 }
669 }
670
671 void mouseReleased() {
672 for (UIContext context : overlays) {
673 context.mouseReleased(mouseX, mouseY);
674 }
675 }
676
677 void mouseWheel(int delta) {
678 boolean wheeled = false;
679 for (UIContext context : overlays) {
680 wheeled |= context.mouseWheel(mouseX, mouseY, delta);
681 }
682
683 if (!wheeled) {
684 eyeR = constrain(eyeR - delta, -500, -80);
685 eyeX = midX + eyeR*sin(eyeA);
686 eyeZ = midZ + eyeR*cos(eyeA);
687 }
688 }