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;
62 // Handles to UI objects
64 UIPatternDeck uiPatternA;
65 UICrossfader uiCrossfader;
68 UIDebugText uiDebugText;
71 float eyeR, eyeA, eyeX, eyeY, eyeZ, midX, midY, midZ;
74 * Engine construction and initialization.
76 LXPattern[] _patterns(GLucose glucose) {
77 LXPattern[] patterns = patterns(glucose);
78 for (LXPattern p : patterns) {
79 p.setTransition(new DissolveTransition(glucose.lx).setDuration(1000));
84 void logTime(String evt) {
86 println(evt + ": " + (now - lastMillis) + "ms");
91 startMillis = lastMillis = millis();
93 // Initialize the Processing graphics environment
94 size(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, OPENGL);
95 frameRate(targetFramerate);
97 // hint(ENABLE_OPENGL_4X_SMOOTH); // no discernable improvement?
98 logTime("Created viewport");
100 // Create the GLucose engine to run the cubes
101 glucose = new GLucose(this, buildModel());
103 lx.enableKeyboardTempo();
104 logTime("Built GLucose engine");
107 Engine engine = lx.engine;
108 engine.setPatterns(patterns = _patterns(glucose));
109 engine.addDeck(_patterns(glucose));
110 logTime("Built patterns");
111 glucose.setTransitions(transitions(glucose));
112 logTime("Built transitions");
113 glucose.lx.addEffects(effects(glucose));
114 logTime("Built effects");
116 // Build output driver
117 PandaMapping[] pandaMappings = buildPandaList();
118 pandaBoards = new PandaDriver[pandaMappings.length];
120 for (PandaMapping pm : pandaMappings) {
121 pandaBoards[pbi++] = new PandaDriver(pm.ip, glucose.model, pm);
123 mappingTool = new MappingTool(glucose, pandaMappings);
124 logTime("Built PandaDriver");
127 List<MidiListener> midiListeners = new ArrayList<MidiListener>();
128 midiListeners.add(midiQwertyKeys = new MidiListener(MidiListener.KEYS));
129 midiListeners.add(midiQwertyAPC = new MidiListener(MidiListener.APC));
130 for (MidiInputDevice device : RWMidi.getInputDevices()) {
131 boolean enableDevice = device.getName().contains("APC");
132 midiListeners.add(new MidiListener(device).setEnabled(enableDevice));
134 SCMidiDevices.initializeStandardDevices(glucose);
135 logTime("Setup MIDI devices");
138 debugUI = new DebugUI(pandaMappings);
139 overlays = new UIContext[] {
140 uiPatternA = new UIPatternDeck(lx.engine.getDeck(0), "PATTERN A", 4, 4, 140, 324),
141 new UIBlendMode(4, 332, 140, 86),
142 new UIEffects(4, 422, 140, 144),
143 new UITempo(4, 570, 140, 50),
144 new UISpeed(4, 624, 140, 50),
146 new UIPatternDeck(lx.engine.getDeck(1), "PATTERN B", width-144, 4, 140, 324),
147 uiMidi = new UIMidi(midiListeners, width-144, 332, 140, 158),
148 new UIOutput(width-144, 494, 140, 106),
150 uiCrossfader = new UICrossfader(width/2-90, height-90, 180, 86),
152 uiDebugText = new UIDebugText(148, height-138, width-304, 44),
153 uiMapping = new UIMapping(mappingTool, 4, 4, 140, 324),
155 uiMapping.setVisible(false);
156 logTime("Built overlay UI");
159 logo = loadImage("data/logo.png");
162 midX = TRAILER_WIDTH/2.;
163 midY = glucose.model.yMax/2;
164 midZ = TRAILER_DEPTH/2.;
168 eyeX = midX + eyeR*sin(eyeA);
169 eyeZ = midZ + eyeR*cos(eyeA);
170 addMouseWheelListener(new java.awt.event.MouseWheelListener() {
171 public void mouseWheelMoved(java.awt.event.MouseWheelEvent mwe) {
172 mouseWheel(mwe.getWheelRotation());
175 println("Total setup: " + (millis() - startMillis) + "ms");
176 println("Hit the 'p' key to toggle Panda Board output");
179 public class MidiListener extends AbstractScrollItem {
181 public static final int MIDI = 0;
182 public static final int KEYS = 1;
183 public static final int APC = 2;
185 private boolean enabled = false;
186 private final String name;
188 MidiListener(MidiInputDevice d) {
197 NoteMeta(int channel, int number) {
198 this.channel = channel;
199 this.number = number;
203 final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();
205 private final int mode;
206 private int octaveShift = 0;
208 MidiListener(int mode) {
212 name = "QWERTY (APC Mode)";
217 name = "QWERTY (Key Mode)";
223 private void mapAPC() {
248 registerKeyEvent(this);
251 private void mapKeys() {
253 mapNote('a', 1, note++);
254 mapNote('w', 1, note++);
255 mapNote('s', 1, note++);
256 mapNote('e', 1, note++);
257 mapNote('d', 1, note++);
258 mapNote('f', 1, note++);
259 mapNote('t', 1, note++);
260 mapNote('g', 1, note++);
261 mapNote('y', 1, note++);
262 mapNote('h', 1, note++);
263 mapNote('u', 1, note++);
264 mapNote('j', 1, note++);
265 mapNote('k', 1, note++);
266 mapNote('o', 1, note++);
267 mapNote('l', 1, note++);
268 registerKeyEvent(this);
271 void mapNote(char ch, int channel, int number) {
272 keyToNote.put(ch, new NoteMeta(channel, number));
275 public String getLabel() {
279 public void keyEvent(KeyEvent e) {
283 char c = Character.toLowerCase(e.getKeyChar());
284 NoteMeta nm = keyToNote.get(c);
287 case KeyEvent.KEY_PRESSED:
288 noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
290 case KeyEvent.KEY_RELEASED:
291 noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
295 if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
298 octaveShift = constrain(octaveShift-1, -4, 4);
301 octaveShift = constrain(octaveShift+1, -4, 4);
307 public boolean isEnabled() {
311 public boolean isSelected() {
315 public void onMousePressed() {
316 setEnabled(!enabled);
319 public MidiListener setEnabled(boolean enabled) {
320 if (enabled != this.enabled) {
321 this.enabled = enabled;
322 if (uiMidi != null) uiMidi.redraw();
327 private SCPattern getFocusedPattern() {
328 return (SCPattern) uiMidi.getFocusedDeck().getActivePattern();
331 void programChangeReceived(ProgramChange pc) {
335 if (uiMidi.logMidi()) {
336 println(getLabel() + " :: Program Change :: " + pc.getNumber());
340 void controllerChangeReceived(rwmidi.Controller cc) {
344 if (uiMidi.logMidi()) {
345 println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue());
347 getFocusedPattern().controllerChangeReceived(cc);
350 void noteOnReceived(Note note) {
354 if (uiMidi.logMidi()) {
355 println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
357 getFocusedPattern().noteOnReceived(note);
360 void noteOffReceived(Note note) {
364 if (uiMidi.logMidi()) {
365 println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
367 getFocusedPattern().noteOffReceived(note);
373 * Core render loop and drawing functionality.
376 // Draws the simulation and the 2D UI overlay
378 color[] colors = glucose.getColors();
380 String displayMode = uiCrossfader.getDisplayMode();
381 if (displayMode == "A") {
382 colors = lx.engine.getDeck(0).getColors();
383 } else if (displayMode == "B") {
384 colors = lx.engine.getDeck(1).getColors();
387 debugUI.maskColors(colors);
400 drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
405 vertex(TRAILER_WIDTH, 0, 0);
406 vertex(TRAILER_WIDTH, 0, TRAILER_DEPTH);
407 vertex(0, 0, TRAILER_DEPTH);
410 // Draw the logo on the front of platform
415 image(logo, TRAILER_WIDTH/2/s-logo.width/2, TRAILER_HEIGHT/2/s-logo.height/2-2/s);
419 // drawBassBox(glucose.model.bassBox);
420 // for (Speaker s : glucose.model.speakers) {
423 for (Cube c : glucose.model.cubes) {
430 // TODO(mcslee): restore when bassBox/speakers are right again
431 // for (Point p : glucose.model.points) {
432 for (Cube cube : glucose.model.cubes) {
433 for (Point p : cube.points) {
434 stroke(colors[p.index]);
435 vertex(p.fx, p.fy, p.fz);
443 // Send output colors
444 color[] sendColors = glucose.getColors();
446 debugUI.maskColors(colors);
449 // Gamma correction here. Apply a cubic to the brightness
450 // for better representation of dynamic range
451 for (int i = 0; i < colors.length; ++i) {
452 float b = brightness(colors[i]) / 100.f;
455 saturation(colors[i]),
460 // TODO(mcslee): move into GLucose engine
461 for (PandaDriver p : pandaBoards) {
466 void drawBassBox(BassBox b) {
472 translate(b.x + BassBox.EDGE_WIDTH/2., b.y + BassBox.EDGE_HEIGHT/2, b.z + BassBox.EDGE_DEPTH/2.);
473 box(BassBox.EDGE_WIDTH-20*in, BassBox.EDGE_HEIGHT-20*in, BassBox.EDGE_DEPTH-20*in);
478 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);
481 translate(b.x+(Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT-in, b.z + BassBox.EDGE_DEPTH/2.);
482 float lastOffset = 0;
483 for (float offset : BoothFloor.STRIP_OFFSETS) {
484 translate(offset - lastOffset, 0, 0);
485 box(Cube.CHANNEL_WIDTH-in, 0, BassBox.EDGE_DEPTH - 2*in);
491 translate(b.x + (Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT/2., b.z + in);
492 for (int j = 0; j < 2; ++j) {
494 for (int i = 0; i < BassBox.NUM_FRONT_STRUTS; ++i) {
495 translate(BassBox.FRONT_STRUT_SPACING, 0, 0);
496 box(Cube.CHANNEL_WIDTH-in, BassBox.EDGE_HEIGHT - in*2, 0);
499 translate(0, 0, BassBox.EDGE_DEPTH - 2*in);
504 translate(b.x + in, b.y + BassBox.EDGE_HEIGHT/2., b.z + BassBox.SIDE_STRUT_SPACING + (Cube.CHANNEL_WIDTH-in)/2.);
505 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
506 translate(BassBox.EDGE_WIDTH-2*in, 0, 0);
507 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
512 void drawCube(Cube c) {
516 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);
519 void drawSpeaker(Speaker s) {
525 translate(s.x, s.y, s.z);
526 rotate(s.ry / 180. * PI, 0, -1, 0);
527 translate(Speaker.EDGE_WIDTH/2., Speaker.EDGE_HEIGHT/2., Speaker.EDGE_DEPTH/2.);
528 box(Speaker.EDGE_WIDTH-20*in, Speaker.EDGE_HEIGHT-20*in, Speaker.EDGE_DEPTH-20*in);
529 translate(0, Speaker.EDGE_HEIGHT/2. + Speaker.EDGE_HEIGHT*.8/2, 0);
532 box(Speaker.EDGE_WIDTH*.6, Speaker.EDGE_HEIGHT*.8, Speaker.EDGE_DEPTH*.75);
537 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);
540 void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, float yd, float zd, float sw) {
543 rotate(rx / 180. * PI, -1, 0, 0);
544 rotate(ry / 180. * PI, 0, -1, 0);
545 rotate(rz / 180. * PI, 0, 0, -1);
546 for (int i = 0; i < 4; ++i) {
547 float wid = (i % 2 == 0) ? xd : zd;
553 vertex(wid - sw, yd);
554 vertex(wid - sw, sw);
560 vertex(wid - sw, yd);
561 vertex(wid - sw, yd - sw);
566 translate(wid, 0, 0);
567 rotate(HALF_PI, 0, -1, 0);
574 javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
575 gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
576 ((PGraphicsOpenGL)g).endGL();
580 for (UIContext context : overlays) {
585 // Always draw FPS meter
588 textAlign(LEFT, BASELINE);
589 text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
598 * Top-level keyboard event handling
602 mappingTool.keyPressed(uiMapping);
607 frameRate(--targetFramerate);
611 frameRate(++targetFramerate);
614 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
615 debugMode = !debugMode;
616 println("Debug output: " + (debugMode ? "ON" : "OFF"));
620 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
621 mappingMode = !mappingMode;
622 uiPatternA.setVisible(!mappingMode);
623 uiMapping.setVisible(mappingMode);
625 restoreToPattern = lx.getPattern();
626 lx.setPatterns(new LXPattern[] { mappingTool });
628 lx.setPatterns(patterns);
629 LXTransition pop = restoreToPattern.getTransition();
630 restoreToPattern.setTransition(null);
631 lx.goPattern(restoreToPattern);
632 restoreToPattern.setTransition(pop);
637 for (PandaDriver p : pandaBoards) {
642 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
650 * Top-level mouse event handling
653 void mousePressed() {
654 boolean debugged = false;
656 debugged = debugUI.mousePressed();
659 for (UIContext context : overlays) {
660 context.mousePressed(mouseX, mouseY);
667 void mouseDragged() {
668 boolean dragged = false;
669 for (UIContext context : overlays) {
670 dragged |= context.mouseDragged(mouseX, mouseY);
673 int dx = mouseX - mx;
674 int dy = mouseY - my;
678 eyeX = midX + eyeR*sin(eyeA);
679 eyeZ = midZ + eyeR*cos(eyeA);
684 void mouseReleased() {
685 for (UIContext context : overlays) {
686 context.mouseReleased(mouseX, mouseY);
690 void mouseWheel(int delta) {
691 boolean wheeled = false;
692 for (UIContext context : overlays) {
693 wheeled |= context.mouseWheel(mouseX, mouseY, delta);
697 eyeR = constrain(eyeR - delta, -500, -80);
698 eyeX = midX + eyeR*sin(eyeA);
699 eyeZ = midZ + eyeR*cos(eyeA);