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 SCMidiInput midiQwertyKeys;
52 SCMidiInput 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<SCMidiInput> midiControllers = new ArrayList<SCMidiInput>();
128 midiControllers.add(midiQwertyKeys = new SCMidiInput(SCMidiInput.KEYS));
129 midiControllers.add(midiQwertyAPC = new SCMidiInput(SCMidiInput.APC));
130 for (MidiInputDevice device : RWMidi.getInputDevices()) {
131 boolean enableDevice = device.getName().contains("APC");
132 midiControllers.add(new SCMidiInput(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(midiControllers, 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 interface SCMidiInputListener {
180 public void onEnabled(SCMidiInput controller, boolean enabled);
183 public class SCMidiInput extends AbstractScrollItem {
185 public static final int MIDI = 0;
186 public static final int KEYS = 1;
187 public static final int APC = 2;
189 private boolean enabled = false;
190 private final String name;
191 private final int mode;
192 private int octaveShift = 0;
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 final List<SCMidiInputListener> listeners = new ArrayList<SCMidiInputListener>();
207 public SCMidiInput addListener(SCMidiInputListener l) {
212 public SCMidiInput removeListener(SCMidiInputListener l) {
217 SCMidiInput(MidiInputDevice d) {
220 name = d.getName().replace("Unknown vendor","");
223 SCMidiInput(int mode) {
227 name = "QWERTY (APC Mode)";
232 name = "QWERTY (Key Mode)";
238 private void mapAPC() {
263 registerKeyEvent(this);
266 private void mapKeys() {
268 mapNote('a', 1, note++);
269 mapNote('w', 1, note++);
270 mapNote('s', 1, note++);
271 mapNote('e', 1, note++);
272 mapNote('d', 1, note++);
273 mapNote('f', 1, note++);
274 mapNote('t', 1, note++);
275 mapNote('g', 1, note++);
276 mapNote('y', 1, note++);
277 mapNote('h', 1, note++);
278 mapNote('u', 1, note++);
279 mapNote('j', 1, note++);
280 mapNote('k', 1, note++);
281 mapNote('o', 1, note++);
282 mapNote('l', 1, note++);
283 registerKeyEvent(this);
286 void mapNote(char ch, int channel, int number) {
287 keyToNote.put(ch, new NoteMeta(channel, number));
290 public String getLabel() {
294 public void keyEvent(KeyEvent e) {
298 char c = Character.toLowerCase(e.getKeyChar());
299 NoteMeta nm = keyToNote.get(c);
302 case KeyEvent.KEY_PRESSED:
303 noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
305 case KeyEvent.KEY_RELEASED:
306 noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
310 if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
313 octaveShift = constrain(octaveShift-1, -4, 4);
316 octaveShift = constrain(octaveShift+1, -4, 4);
322 public boolean isEnabled() {
326 public boolean isSelected() {
330 public void onMousePressed() {
331 setEnabled(!enabled);
334 public SCMidiInput setEnabled(boolean enabled) {
335 if (enabled != this.enabled) {
336 this.enabled = enabled;
337 for (SCMidiInputListener l : listeners) {
338 l.onEnabled(this, enabled);
344 private SCPattern getFocusedPattern() {
345 Engine.Deck focusedDeck = (uiMidi != null) ? uiMidi.getFocusedDeck() : lx.engine.getDefaultDeck();
346 return (SCPattern) focusedDeck.getActivePattern();
349 private boolean logMidi() {
350 return (uiMidi != null) && uiMidi.logMidi();
353 void programChangeReceived(ProgramChange pc) {
358 println(getLabel() + " :: Program Change :: " + pc.getNumber());
362 void controllerChangeReceived(rwmidi.Controller cc) {
367 println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue());
369 getFocusedPattern().controllerChangeReceived(cc);
372 void noteOnReceived(Note note) {
377 println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
379 getFocusedPattern().noteOnReceived(note);
382 void noteOffReceived(Note note) {
387 println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
389 getFocusedPattern().noteOffReceived(note);
395 * Core render loop and drawing functionality.
398 // Draws the simulation and the 2D UI overlay
400 color[] colors = glucose.getColors();
402 String displayMode = uiCrossfader.getDisplayMode();
403 if (displayMode == "A") {
404 colors = lx.engine.getDeck(0).getColors();
405 } else if (displayMode == "B") {
406 colors = lx.engine.getDeck(1).getColors();
409 debugUI.maskColors(colors);
422 drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
427 vertex(TRAILER_WIDTH, 0, 0);
428 vertex(TRAILER_WIDTH, 0, TRAILER_DEPTH);
429 vertex(0, 0, TRAILER_DEPTH);
432 // Draw the logo on the front of platform
437 image(logo, TRAILER_WIDTH/2/s-logo.width/2, TRAILER_HEIGHT/2/s-logo.height/2-2/s);
441 // drawBassBox(glucose.model.bassBox);
442 // for (Speaker s : glucose.model.speakers) {
445 for (Cube c : glucose.model.cubes) {
452 // TODO(mcslee): restore when bassBox/speakers are right again
453 // for (Point p : glucose.model.points) {
454 for (Cube cube : glucose.model.cubes) {
455 for (Point p : cube.points) {
456 stroke(colors[p.index]);
457 vertex(p.fx, p.fy, p.fz);
465 // Send output colors
466 color[] sendColors = glucose.getColors();
468 debugUI.maskColors(colors);
471 // Gamma correction here. Apply a cubic to the brightness
472 // for better representation of dynamic range
473 for (int i = 0; i < colors.length; ++i) {
474 float b = brightness(colors[i]) / 100.f;
477 saturation(colors[i]),
482 // TODO(mcslee): move into GLucose engine
483 for (PandaDriver p : pandaBoards) {
488 void drawBassBox(BassBox b) {
494 translate(b.x + BassBox.EDGE_WIDTH/2., b.y + BassBox.EDGE_HEIGHT/2, b.z + BassBox.EDGE_DEPTH/2.);
495 box(BassBox.EDGE_WIDTH-20*in, BassBox.EDGE_HEIGHT-20*in, BassBox.EDGE_DEPTH-20*in);
500 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);
503 translate(b.x+(Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT-in, b.z + BassBox.EDGE_DEPTH/2.);
504 float lastOffset = 0;
505 for (float offset : BoothFloor.STRIP_OFFSETS) {
506 translate(offset - lastOffset, 0, 0);
507 box(Cube.CHANNEL_WIDTH-in, 0, BassBox.EDGE_DEPTH - 2*in);
513 translate(b.x + (Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT/2., b.z + in);
514 for (int j = 0; j < 2; ++j) {
516 for (int i = 0; i < BassBox.NUM_FRONT_STRUTS; ++i) {
517 translate(BassBox.FRONT_STRUT_SPACING, 0, 0);
518 box(Cube.CHANNEL_WIDTH-in, BassBox.EDGE_HEIGHT - in*2, 0);
521 translate(0, 0, BassBox.EDGE_DEPTH - 2*in);
526 translate(b.x + in, b.y + BassBox.EDGE_HEIGHT/2., b.z + BassBox.SIDE_STRUT_SPACING + (Cube.CHANNEL_WIDTH-in)/2.);
527 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
528 translate(BassBox.EDGE_WIDTH-2*in, 0, 0);
529 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
534 void drawCube(Cube c) {
538 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);
541 void drawSpeaker(Speaker s) {
547 translate(s.x, s.y, s.z);
548 rotate(s.ry / 180. * PI, 0, -1, 0);
549 translate(Speaker.EDGE_WIDTH/2., Speaker.EDGE_HEIGHT/2., Speaker.EDGE_DEPTH/2.);
550 box(Speaker.EDGE_WIDTH-20*in, Speaker.EDGE_HEIGHT-20*in, Speaker.EDGE_DEPTH-20*in);
551 translate(0, Speaker.EDGE_HEIGHT/2. + Speaker.EDGE_HEIGHT*.8/2, 0);
554 box(Speaker.EDGE_WIDTH*.6, Speaker.EDGE_HEIGHT*.8, Speaker.EDGE_DEPTH*.75);
559 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);
562 void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, float yd, float zd, float sw) {
565 rotate(rx / 180. * PI, -1, 0, 0);
566 rotate(ry / 180. * PI, 0, -1, 0);
567 rotate(rz / 180. * PI, 0, 0, -1);
568 for (int i = 0; i < 4; ++i) {
569 float wid = (i % 2 == 0) ? xd : zd;
575 vertex(wid - sw, yd);
576 vertex(wid - sw, sw);
582 vertex(wid - sw, yd);
583 vertex(wid - sw, yd - sw);
588 translate(wid, 0, 0);
589 rotate(HALF_PI, 0, -1, 0);
596 javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
597 gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
598 ((PGraphicsOpenGL)g).endGL();
602 for (UIContext context : overlays) {
607 // Always draw FPS meter
610 textAlign(LEFT, BASELINE);
611 text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
620 * Top-level keyboard event handling
624 mappingTool.keyPressed(uiMapping);
629 frameRate(--targetFramerate);
633 frameRate(++targetFramerate);
636 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
637 debugMode = !debugMode;
638 println("Debug output: " + (debugMode ? "ON" : "OFF"));
642 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
643 mappingMode = !mappingMode;
644 uiPatternA.setVisible(!mappingMode);
645 uiMapping.setVisible(mappingMode);
647 restoreToPattern = lx.getPattern();
648 lx.setPatterns(new LXPattern[] { mappingTool });
650 lx.setPatterns(patterns);
651 LXTransition pop = restoreToPattern.getTransition();
652 restoreToPattern.setTransition(null);
653 lx.goPattern(restoreToPattern);
654 restoreToPattern.setTransition(pop);
659 for (PandaDriver p : pandaBoards) {
664 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
672 * Top-level mouse event handling
675 void mousePressed() {
676 boolean debugged = false;
678 debugged = debugUI.mousePressed();
681 for (UIContext context : overlays) {
682 context.mousePressed(mouseX, mouseY);
689 void mouseDragged() {
690 boolean dragged = false;
691 for (UIContext context : overlays) {
692 dragged |= context.mouseDragged(mouseX, mouseY);
695 int dx = mouseX - mx;
696 int dy = mouseY - my;
700 eyeX = midX + eyeR*sin(eyeA);
701 eyeZ = midZ + eyeR*cos(eyeA);
706 void mouseReleased() {
707 for (UIContext context : overlays) {
708 context.mouseReleased(mouseX, mouseY);
712 void mouseWheel(int delta) {
713 boolean wheeled = false;
714 for (UIContext context : overlays) {
715 wheeled |= context.mouseWheel(mouseX, mouseY, delta);
719 eyeR = constrain(eyeR - delta, -500, -80);
720 eyeX = midX + eyeR*sin(eyeA);
721 eyeZ = midZ + eyeR*cos(eyeA);