2 * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND
5 * ///\\\ ///\\\ ///\\\ ///\\\
6 * \\\/// \\\/// \\\/// \\\///
9 * EXPERTS ONLY!! EXPERTS ONLY!!
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.
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.*;
30 import ddf.minim.analysis.*;
31 import processing.opengl.*;
34 final int VIEWPORT_WIDTH = 900;
35 final int VIEWPORT_HEIGHT = 700;
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;
42 int targetFramerate = 60;
43 int startMillis, lastMillis;
45 // Core engine variables
49 MappingTool mappingTool;
50 PandaDriver[] pandaBoards;
51 MidiListener midiQwertyKeys;
52 MidiListener midiQwertyAPC;
54 // Display configuration mode
55 boolean mappingMode = false;
56 boolean debugMode = false;
59 LXPattern restoreToPattern = null;
61 // Handles to UI objects
63 UIPatternDeck uiPatternA;
64 UICrossfader uiCrossfader;
67 UIDebugText uiDebugText;
70 float eyeR, eyeA, eyeX, eyeY, eyeZ, midX, midY, midZ;
73 * Engine construction and initialization.
75 LXPattern[] _patterns(GLucose glucose) {
76 LXPattern[] patterns = patterns(glucose);
77 for (LXPattern p : patterns) {
78 p.setTransition(new DissolveTransition(glucose.lx).setDuration(1000));
83 void logTime(String evt) {
85 println(evt + ": " + (now - lastMillis) + "ms");
90 startMillis = lastMillis = millis();
92 // Initialize the Processing graphics environment
93 size(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, OPENGL);
94 frameRate(targetFramerate);
96 // hint(ENABLE_OPENGL_4X_SMOOTH); // no discernable improvement?
97 logTime("Created viewport");
99 // Create the GLucose engine to run the cubes
100 glucose = new GLucose(this, buildModel());
102 lx.enableKeyboardTempo();
103 logTime("Built GLucose engine");
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");
115 // Build output driver
116 PandaMapping[] pandaMappings = buildPandaList();
117 pandaBoards = new PandaDriver[pandaMappings.length];
119 for (PandaMapping pm : pandaMappings) {
120 pandaBoards[pbi++] = new PandaDriver(pm.ip, glucose.model, pm);
122 mappingTool = new MappingTool(glucose, pandaMappings);
123 logTime("Built PandaDriver");
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));
133 SCMidiDevices.initializeStandardDevices(glucose);
134 logTime("Setup MIDI devices");
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),
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),
149 uiCrossfader = new UICrossfader(width/2-90, height-90, 180, 86),
151 uiDebugText = new UIDebugText(148, height-138, width-304, 44),
152 uiMapping = new UIMapping(mappingTool, 4, 4, 140, 324),
154 uiMapping.setVisible(false);
155 logTime("Built overlay UI");
158 midX = TRAILER_WIDTH/2.;
159 midY = glucose.model.yMax/2;
160 midZ = TRAILER_DEPTH/2.;
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());
171 println("Total setup: " + (millis() - startMillis) + "ms");
172 println("Hit the 'p' key to toggle Panda Board output");
175 public class MidiListener extends AbstractScrollItem {
177 public static final int MIDI = 0;
178 public static final int KEYS = 1;
179 public static final int APC = 2;
181 private boolean enabled = false;
182 private final String name;
184 MidiListener(MidiInputDevice d) {
193 NoteMeta(int channel, int number) {
194 this.channel = channel;
195 this.number = number;
199 final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();
201 private final int mode;
202 private int octaveShift = 0;
204 MidiListener(int mode) {
208 name = "QWERTY (APC Mode)";
213 name = "QWERTY (Key Mode)";
219 private void mapAPC() {
244 registerKeyEvent(this);
247 private void mapKeys() {
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);
267 void mapNote(char ch, int channel, int number) {
268 keyToNote.put(ch, new NoteMeta(channel, number));
271 public String getLabel() {
275 public void keyEvent(KeyEvent e) {
279 char c = Character.toLowerCase(e.getKeyChar());
280 NoteMeta nm = keyToNote.get(c);
283 case KeyEvent.KEY_PRESSED:
284 noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
286 case KeyEvent.KEY_RELEASED:
287 noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
291 if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
294 octaveShift = constrain(octaveShift-1, -4, 4);
297 octaveShift = constrain(octaveShift+1, -4, 4);
303 public boolean isEnabled() {
307 public boolean isSelected() {
311 public void onMousePressed() {
312 setEnabled(!enabled);
315 public MidiListener setEnabled(boolean enabled) {
316 if (enabled != this.enabled) {
317 this.enabled = enabled;
323 private SCPattern getFocusedPattern() {
324 return (SCPattern) uiMidi.getFocusedDeck().getActivePattern();
327 void programChangeReceived(ProgramChange pc) {
331 if (uiMidi.logMidi()) {
332 println(getLabel() + " :: Program Change :: " + pc.getNumber());
336 void controllerChangeReceived(rwmidi.Controller cc) {
340 if (uiMidi.logMidi()) {
341 println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue());
343 getFocusedPattern().controllerChangeReceived(cc);
346 void noteOnReceived(Note note) {
350 if (uiMidi.logMidi()) {
351 println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
353 getFocusedPattern().noteOnReceived(note);
356 void noteOffReceived(Note note) {
360 if (uiMidi.logMidi()) {
361 println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
363 getFocusedPattern().noteOffReceived(note);
369 * Core render loop and drawing functionality.
372 // Draws the simulation and the 2D UI overlay
374 color[] colors = glucose.getColors();
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();
383 debugUI.maskColors(colors);
396 drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
401 vertex(TRAILER_WIDTH, 0, 0);
402 vertex(TRAILER_WIDTH, 0, TRAILER_DEPTH);
403 vertex(0, 0, TRAILER_DEPTH);
407 // drawBassBox(glucose.model.bassBox);
408 // for (Speaker s : glucose.model.speakers) {
411 for (Cube c : glucose.model.cubes) {
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);
431 // Send output colors
432 color[] sendColors = glucose.getColors();
434 debugUI.maskColors(colors);
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;
443 saturation(colors[i]),
448 // TODO(mcslee): move into GLucose engine
449 for (PandaDriver p : pandaBoards) {
454 void drawBassBox(BassBox b) {
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);
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);
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);
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) {
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);
487 translate(0, 0, BassBox.EDGE_DEPTH - 2*in);
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);
500 void drawCube(Cube c) {
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);
507 void drawSpeaker(Speaker s) {
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);
520 box(Speaker.EDGE_WIDTH*.6, Speaker.EDGE_HEIGHT*.8, Speaker.EDGE_DEPTH*.75);
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);
528 void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, float yd, float zd, float sw) {
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;
541 vertex(wid - sw, yd);
542 vertex(wid - sw, sw);
548 vertex(wid - sw, yd);
549 vertex(wid - sw, yd - sw);
554 translate(wid, 0, 0);
555 rotate(HALF_PI, 0, -1, 0);
562 javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
563 gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
564 ((PGraphicsOpenGL)g).endGL();
568 for (UIContext context : overlays) {
573 // Always draw FPS meter
576 textAlign(LEFT, BASELINE);
577 text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
585 * Top-level keyboard event handling
589 mappingTool.keyPressed(uiMapping);
594 frameRate(--targetFramerate);
598 frameRate(++targetFramerate);
601 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
602 debugMode = !debugMode;
603 println("Debug output: " + (debugMode ? "ON" : "OFF"));
607 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
608 mappingMode = !mappingMode;
609 uiPatternA.setVisible(!mappingMode);
610 uiMapping.setVisible(mappingMode);
612 restoreToPattern = lx.getPattern();
613 lx.setPatterns(new LXPattern[] { mappingTool });
615 lx.setPatterns(patterns);
616 LXTransition pop = restoreToPattern.getTransition();
617 restoreToPattern.setTransition(null);
618 lx.goPattern(restoreToPattern);
619 restoreToPattern.setTransition(pop);
624 for (PandaDriver p : pandaBoards) {
629 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
637 * Top-level mouse event handling
640 void mousePressed() {
641 boolean debugged = false;
643 debugged = debugUI.mousePressed();
646 for (UIContext context : overlays) {
647 context.mousePressed(mouseX, mouseY);
654 void mouseDragged() {
655 boolean dragged = false;
656 for (UIContext context : overlays) {
657 dragged |= context.mouseDragged(mouseX, mouseY);
660 int dx = mouseX - mx;
661 int dy = mouseY - my;
665 eyeX = midX + eyeR*sin(eyeA);
666 eyeZ = midZ + eyeR*cos(eyeA);
671 void mouseReleased() {
672 for (UIContext context : overlays) {
673 context.mouseReleased(mouseX, mouseY);
677 void mouseWheel(int delta) {
678 boolean wheeled = false;
679 for (UIContext context : overlays) {
680 wheeled |= context.mouseWheel(mouseX, mouseY, delta);
684 eyeR = constrain(eyeR - delta, -500, -80);
685 eyeX = midX + eyeR*sin(eyeA);
686 eyeZ = midZ + eyeR*cos(eyeA);