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