Add midi logging toggle
[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, 160),
145 new UIOutput(width-144, 498, 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 if (uiMidi.logMidi()) {
274 println(getLabel() + " :: Program Change :: " + pc.getNumber());
275 }
276 }
277
278 void controllerChangeReceived(rwmidi.Controller cc) {
279 if (!enabled) {
280 return;
281 }
282 if (uiMidi.logMidi()) {
283 println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue());
284 }
285 getFocusedPattern().controllerChangeReceived(cc);
286 }
287
288 void noteOnReceived(Note note) {
289 if (!enabled) {
290 return;
291 }
292 if (uiMidi.logMidi()) {
293 println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
294 }
295 getFocusedPattern().noteOnReceived(note);
296 }
297
298 void noteOffReceived(Note note) {
299 if (!enabled) {
300 return;
301 }
302 if (uiMidi.logMidi()) {
303 println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
304 }
305 getFocusedPattern().noteOffReceived(note);
306 }
307
308 }
309
310 /**
311 * Core render loop and drawing functionality.
312 */
313 void draw() {
314 // Draws the simulation and the 2D UI overlay
315 background(40);
316 color[] colors = glucose.getColors();
317
318 String displayMode = uiCrossfader.getDisplayMode();
319 if (displayMode == "A") {
320 colors = lx.engine.getDeck(0).getColors();
321 } else if (displayMode == "B") {
322 colors = lx.engine.getDeck(1).getColors();
323 }
324 if (debugMode) {
325 debugUI.maskColors(colors);
326 }
327
328 camera(
329 eyeX, eyeY, eyeZ,
330 midX, midY, midZ,
331 0, -1, 0
332 );
333
334 translate(0, 40, 0);
335
336 noStroke();
337 fill(#141414);
338 drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
339 fill(#070707);
340 stroke(#222222);
341 beginShape();
342 vertex(0, 0, 0);
343 vertex(TRAILER_WIDTH, 0, 0);
344 vertex(TRAILER_WIDTH, 0, TRAILER_DEPTH);
345 vertex(0, 0, TRAILER_DEPTH);
346 endShape();
347
348 noStroke();
349 // drawBassBox(glucose.model.bassBox);
350 // for (Speaker s : glucose.model.speakers) {
351 // drawSpeaker(s);
352 // }
353 for (Cube c : glucose.model.cubes) {
354 drawCube(c);
355 }
356
357 noFill();
358 strokeWeight(2);
359 beginShape(POINTS);
360 // TODO(mcslee): restore when bassBox/speakers are right again
361 // for (Point p : glucose.model.points) {
362 for (Cube cube : glucose.model.cubes) {
363 for (Point p : cube.points) {
364 stroke(colors[p.index]);
365 vertex(p.fx, p.fy, p.fz);
366 }
367 }
368 endShape();
369
370 // 2D Overlay UI
371 drawUI();
372
373 // Send output colors
374 color[] sendColors = glucose.getColors();
375 if (debugMode) {
376 debugUI.maskColors(colors);
377 }
378
379 // Gamma correction here. Apply a cubic to the brightness
380 // for better representation of dynamic range
381 for (int i = 0; i < colors.length; ++i) {
382 float b = brightness(colors[i]) / 100.f;
383 colors[i] = color(
384 hue(colors[i]),
385 saturation(colors[i]),
386 (b*b*b) * 100.
387 );
388 }
389
390 // TODO(mcslee): move into GLucose engine
391 for (PandaDriver p : pandaBoards) {
392 p.send(colors);
393 }
394 }
395
396 void drawBassBox(BassBox b) {
397 float in = .15;
398
399 noStroke();
400 fill(#191919);
401 pushMatrix();
402 translate(b.x + BassBox.EDGE_WIDTH/2., b.y + BassBox.EDGE_HEIGHT/2, b.z + BassBox.EDGE_DEPTH/2.);
403 box(BassBox.EDGE_WIDTH-20*in, BassBox.EDGE_HEIGHT-20*in, BassBox.EDGE_DEPTH-20*in);
404 popMatrix();
405
406 noStroke();
407 fill(#393939);
408 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);
409
410 pushMatrix();
411 translate(b.x+(Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT-in, b.z + BassBox.EDGE_DEPTH/2.);
412 float lastOffset = 0;
413 for (float offset : BoothFloor.STRIP_OFFSETS) {
414 translate(offset - lastOffset, 0, 0);
415 box(Cube.CHANNEL_WIDTH-in, 0, BassBox.EDGE_DEPTH - 2*in);
416 lastOffset = offset;
417 }
418 popMatrix();
419
420 pushMatrix();
421 translate(b.x + (Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT/2., b.z + in);
422 for (int j = 0; j < 2; ++j) {
423 pushMatrix();
424 for (int i = 0; i < BassBox.NUM_FRONT_STRUTS; ++i) {
425 translate(BassBox.FRONT_STRUT_SPACING, 0, 0);
426 box(Cube.CHANNEL_WIDTH-in, BassBox.EDGE_HEIGHT - in*2, 0);
427 }
428 popMatrix();
429 translate(0, 0, BassBox.EDGE_DEPTH - 2*in);
430 }
431 popMatrix();
432
433 pushMatrix();
434 translate(b.x + in, b.y + BassBox.EDGE_HEIGHT/2., b.z + BassBox.SIDE_STRUT_SPACING + (Cube.CHANNEL_WIDTH-in)/2.);
435 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
436 translate(BassBox.EDGE_WIDTH-2*in, 0, 0);
437 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
438 popMatrix();
439
440 }
441
442 void drawCube(Cube c) {
443 float in = .15;
444 noStroke();
445 fill(#393939);
446 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);
447 }
448
449 void drawSpeaker(Speaker s) {
450 float in = .15;
451
452 noStroke();
453 fill(#191919);
454 pushMatrix();
455 translate(s.x, s.y, s.z);
456 rotate(s.ry / 180. * PI, 0, -1, 0);
457 translate(Speaker.EDGE_WIDTH/2., Speaker.EDGE_HEIGHT/2., Speaker.EDGE_DEPTH/2.);
458 box(Speaker.EDGE_WIDTH-20*in, Speaker.EDGE_HEIGHT-20*in, Speaker.EDGE_DEPTH-20*in);
459 translate(0, Speaker.EDGE_HEIGHT/2. + Speaker.EDGE_HEIGHT*.8/2, 0);
460
461 fill(#222222);
462 box(Speaker.EDGE_WIDTH*.6, Speaker.EDGE_HEIGHT*.8, Speaker.EDGE_DEPTH*.75);
463 popMatrix();
464
465 noStroke();
466 fill(#393939);
467 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);
468 }
469
470 void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, float yd, float zd, float sw) {
471 pushMatrix();
472 translate(x, y, z);
473 rotate(rx / 180. * PI, -1, 0, 0);
474 rotate(ry / 180. * PI, 0, -1, 0);
475 rotate(rz / 180. * PI, 0, 0, -1);
476 for (int i = 0; i < 4; ++i) {
477 float wid = (i % 2 == 0) ? xd : zd;
478
479 beginShape();
480 vertex(0, 0);
481 vertex(wid, 0);
482 vertex(wid, yd);
483 vertex(wid - sw, yd);
484 vertex(wid - sw, sw);
485 vertex(0, sw);
486 endShape();
487 beginShape();
488 vertex(0, sw);
489 vertex(0, yd);
490 vertex(wid - sw, yd);
491 vertex(wid - sw, yd - sw);
492 vertex(sw, yd - sw);
493 vertex(sw, sw);
494 endShape();
495
496 translate(wid, 0, 0);
497 rotate(HALF_PI, 0, -1, 0);
498 }
499 popMatrix();
500 }
501
502 void drawUI() {
503 camera();
504 javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
505 gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
506 ((PGraphicsOpenGL)g).endGL();
507 strokeWeight(1);
508
509 if (uiOn) {
510 for (UIContext context : overlays) {
511 context.draw();
512 }
513 }
514
515 // Always draw FPS meter
516 fill(#555555);
517 textSize(9);
518 textAlign(LEFT, BASELINE);
519 text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
520
521 if (debugMode) {
522 debugUI.draw();
523 }
524 }
525
526 /**
527 * Top-level keyboard event handling
528 */
529 void keyPressed() {
530 if (mappingMode) {
531 mappingTool.keyPressed(uiMapping);
532 }
533 switch (key) {
534 case '-':
535 case '_':
536 frameRate(--targetFramerate);
537 break;
538 case '=':
539 case '+':
540 frameRate(++targetFramerate);
541 break;
542 case 'd':
543 if (!midiQwerty.isEnabled()) {
544 debugMode = !debugMode;
545 println("Debug output: " + (debugMode ? "ON" : "OFF"));
546 }
547 break;
548 case 'm':
549 if (!midiQwerty.isEnabled()) {
550 mappingMode = !mappingMode;
551 uiPatternA.setVisible(!mappingMode);
552 uiMapping.setVisible(mappingMode);
553 if (mappingMode) {
554 restoreToPattern = lx.getPattern();
555 lx.setPatterns(new LXPattern[] { mappingTool });
556 } else {
557 lx.setPatterns(patterns);
558 LXTransition pop = restoreToPattern.getTransition();
559 restoreToPattern.setTransition(null);
560 lx.goPattern(restoreToPattern);
561 restoreToPattern.setTransition(pop);
562 }
563 }
564 break;
565 case 'p':
566 for (PandaDriver p : pandaBoards) {
567 p.toggle();
568 }
569 break;
570 case 'u':
571 uiOn = !uiOn;
572 break;
573 }
574 }
575
576 /**
577 * Top-level mouse event handling
578 */
579 int mx, my;
580 void mousePressed() {
581 boolean debugged = false;
582 if (debugMode) {
583 debugged = debugUI.mousePressed();
584 }
585 if (!debugged) {
586 for (UIContext context : overlays) {
587 context.mousePressed(mouseX, mouseY);
588 }
589 }
590 mx = mouseX;
591 my = mouseY;
592 }
593
594 void mouseDragged() {
595 boolean dragged = false;
596 for (UIContext context : overlays) {
597 dragged |= context.mouseDragged(mouseX, mouseY);
598 }
599 if (!dragged) {
600 int dx = mouseX - mx;
601 int dy = mouseY - my;
602 mx = mouseX;
603 my = mouseY;
604 eyeA += dx*.003;
605 eyeX = midX + eyeR*sin(eyeA);
606 eyeZ = midZ + eyeR*cos(eyeA);
607 eyeY += dy;
608 }
609 }
610
611 void mouseReleased() {
612 for (UIContext context : overlays) {
613 context.mouseReleased(mouseX, mouseY);
614 }
615 }
616
617 void mouseWheel(int delta) {
618 boolean wheeled = false;
619 for (UIContext context : overlays) {
620 wheeled |= context.mouseWheel(mouseX, mouseY, delta);
621 }
622
623 if (!wheeled) {
624 eyeR = constrain(eyeR - delta, -500, -80);
625 eyeX = midX + eyeR*sin(eyeA);
626 eyeZ = midZ + eyeR*cos(eyeA);
627 }
628 }