Support for dual APC40s
[SugarCubes.git] / _MIDI.pde
index d6e90bfadd8134edb9c6574dfee6896f67aaf89a..2abd30de260961aff679082b277dec2aeee86156 100644 (file)
--- a/_MIDI.pde
+++ b/_MIDI.pde
@@ -43,21 +43,39 @@ class MidiEngine {
 
   public MidiEngine() {
     grid = new GridController(this);
-    midiControllers.add(midiQwertyKeys = new SCMidiInput(this, SCMidiInput.KEYS));
-    midiControllers.add(midiQwertyAPC = new SCMidiInput(this, SCMidiInput.APC));
+    midiControllers.add(midiQwertyKeys = new VirtualKeyMidiInput(this, VirtualKeyMidiInput.KEYS));
+    midiControllers.add(midiQwertyAPC = new VirtualKeyMidiInput(this, VirtualKeyMidiInput.APC));
+    int apcCount = 0;
     for (MidiInputDevice device : RWMidi.getInputDevices()) {
       if (device.getName().contains("APC")) {
-        midiControllers.add(new APC40MidiInput(this, device).setEnabled(true));
+        ++apcCount;
+      }
+    }    
+    
+    int apcIndex = 0;
+    for (MidiInputDevice device : RWMidi.getInputDevices()) {
+      if (device.getName().contains("APC")) {
+        int apcDeck = -1;
+        if (apcCount > 1 && apcIndex < 2) {
+          apcDeck = apcIndex++;
+        }
+        midiControllers.add(new APC40MidiInput(this, device, apcDeck).setEnabled(true));
       } else if (device.getName().contains("SLIDER/KNOB KORG")) {
         midiControllers.add(new KorgNanoKontrolMidiInput(this, device).setEnabled(true));
       } else {
         boolean enabled = device.getName().contains("KEYBOARD KORG") || device.getName().contains("Bus 1 Apple");
-        midiControllers.add(new SCMidiInput(this, device).setEnabled(enabled));
+        midiControllers.add(new GenericDeviceMidiInput(this, device).setEnabled(enabled));
       }
     }
+    
+    apcIndex = 0;
     for (MidiOutputDevice device : RWMidi.getOutputDevices()) {
       if (device.getName().contains("APC")) {
-        new APC40MidiOutput(this, device);
+        int apcDeck = -1;
+        if (apcCount > 1 && apcIndex < 2) {
+          apcDeck = apcIndex++;
+        }
+        new APC40MidiOutput(this, device, apcDeck);
       }
     }
   }
@@ -93,32 +111,20 @@ public interface SCMidiInputListener {
   public void onEnabled(SCMidiInput controller, boolean enabled);
 }
 
-public class SCMidiInput extends AbstractScrollItem {
-
-  public static final int MIDI = 0;
-  public static final int KEYS = 1;
-  public static final int APC = 2;
+public abstract class SCMidiInput extends AbstractScrollItem {
 
-  private boolean enabled = false;
+  protected boolean enabled = false;
   private final String name;
-  private final int mode;
-  private int octaveShift = 0;
 
   protected final MidiEngine midiEngine;
 
-  class NoteMeta {
-    int channel;
-    int number;
-    NoteMeta(int channel, int number) {
-      this.channel = channel;
-      this.number = number;
-    }
-  }
-
-  final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();
-
   final List<SCMidiInputListener> listeners = new ArrayList<SCMidiInputListener>();
 
+  protected SCMidiInput(MidiEngine midiEngine, String name) {
+    this.midiEngine = midiEngine;
+    this.name = name;
+  }
+
   public SCMidiInput addListener(SCMidiInputListener l) {
     listeners.add(l);
     return this;
@@ -129,113 +135,10 @@ public class SCMidiInput extends AbstractScrollItem {
     return this;
   }
 
-  SCMidiInput(MidiEngine midiEngine, MidiInputDevice d) {
-    this.midiEngine = midiEngine;
-    mode = MIDI;
-    d.createInput(this);
-    name = d.getName().replace("Unknown vendor","");
-  }
-
-  SCMidiInput(MidiEngine midiEngine, int mode) {
-    this.midiEngine = midiEngine;
-    this.mode = mode;
-    switch (mode) {
-    case APC:
-      name = "QWERTY (APC Mode)";
-      mapAPC();
-      break;
-    default:
-    case KEYS:
-      name = "QWERTY (Key Mode)";
-      mapKeys();
-      break;
-    }
-  }
-
-  private void mapAPC() {
-    mapNote('1', 0, 53);
-    mapNote('2', 1, 53);
-    mapNote('3', 2, 53);
-    mapNote('4', 3, 53);
-    mapNote('5', 4, 53);
-    mapNote('6', 5, 53);
-    mapNote('q', 0, 54);
-    mapNote('w', 1, 54);
-    mapNote('e', 2, 54);
-    mapNote('r', 3, 54);
-    mapNote('t', 4, 54);
-    mapNote('y', 5, 54);
-    mapNote('a', 0, 55);
-    mapNote('s', 1, 55);
-    mapNote('d', 2, 55);
-    mapNote('f', 3, 55);
-    mapNote('g', 4, 55);
-    mapNote('h', 5, 55);
-    mapNote('z', 0, 56);
-    mapNote('x', 1, 56);
-    mapNote('c', 2, 56);
-    mapNote('v', 3, 56);
-    mapNote('b', 4, 56);
-    mapNote('n', 5, 56);
-    registerKeyEvent(this);
-  }
-
-  private void mapKeys() {
-    int note = 48;
-    mapNote('a', 1, note++);
-    mapNote('w', 1, note++);
-    mapNote('s', 1, note++);
-    mapNote('e', 1, note++);
-    mapNote('d', 1, note++);
-    mapNote('f', 1, note++);
-    mapNote('t', 1, note++);
-    mapNote('g', 1, note++);
-    mapNote('y', 1, note++);
-    mapNote('h', 1, note++);
-    mapNote('u', 1, note++);
-    mapNote('j', 1, note++);
-    mapNote('k', 1, note++);
-    mapNote('o', 1, note++);
-    mapNote('l', 1, note++);
-    registerKeyEvent(this);
-  }
-
-  void mapNote(char ch, int channel, int number) {
-    keyToNote.put(ch, new NoteMeta(channel, number));
-  }
-
   public String getLabel() {
     return name;
   }
 
-  public void keyEvent(KeyEvent e) {
-    if (!enabled) {
-      return;
-    }
-    char c = Character.toLowerCase(e.getKeyChar());
-    NoteMeta nm = keyToNote.get(c);
-    if (nm != null) {
-      switch (e.getID()) {
-      case KeyEvent.KEY_PRESSED:
-        noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
-        break;
-      case KeyEvent.KEY_RELEASED:
-        noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
-        break;
-      }
-    }
-    if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
-      switch (c) {
-      case 'z':
-        octaveShift = constrain(octaveShift-1, -4, 4);
-        break;
-      case 'x':
-        octaveShift = constrain(octaveShift+1, -4, 4);
-        break;
-      }
-    }
-  }
-
   public boolean isEnabled() {
     return enabled;
   }
@@ -261,6 +164,10 @@ public class SCMidiInput extends AbstractScrollItem {
   private boolean logMidi() {
     return (uiMidi != null) && uiMidi.logMidi();
   }
+  
+  protected SCPattern getTargetPattern() {
+    return midiEngine.getFocusedPattern();
+  }
 
   final void programChangeReceived(ProgramChange pc) {
     if (!enabled) {
@@ -280,7 +187,7 @@ public class SCMidiInput extends AbstractScrollItem {
       println(getLabel() + " :: Controller :: " + cc.getChannel() + " :: " + cc.getCC() + ":" + cc.getValue());
     }
     if (!handleGridControllerChange(cc)) {
-      if (!midiEngine.getFocusedPattern().controllerChange(cc)) {
+      if (!getTargetPattern().controllerChange(cc)) {
         handleControllerChange(cc);
       }
     }
@@ -294,7 +201,7 @@ public class SCMidiInput extends AbstractScrollItem {
       println(getLabel() + " :: Note On  :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
     }
     if (!handleGridNoteOn(note)) {
-      if (!midiEngine.getFocusedPattern().noteOn(note)) {
+      if (!getTargetPattern().noteOn(note)) {
         handleNoteOn(note);
       }
     }
@@ -308,7 +215,7 @@ public class SCMidiInput extends AbstractScrollItem {
       println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
     }
     if (!handleGridNoteOff(note)) {
-      if (!midiEngine.getFocusedPattern().noteOff(note)) {
+      if (!getTargetPattern().noteOff(note)) {
         handleNoteOff(note);
       }
     }
@@ -324,13 +231,150 @@ public class SCMidiInput extends AbstractScrollItem {
   protected void handleNoteOff(Note note) {}
 }
 
-public class APC40MidiInput extends SCMidiInput {
+public class VirtualKeyMidiInput extends SCMidiInput {
+
+  public static final int KEYS = 1;
+  public static final int APC = 2;
+  
+  private final int mode;
+  
+  private int octaveShift = 0;
+
+  class NoteMeta {
+    int channel;
+    int number;
+    NoteMeta(int channel, int number) {
+      this.channel = channel;
+      this.number = number;
+    }
+  }
+
+  final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();  
+  
+  VirtualKeyMidiInput(MidiEngine midiEngine, int mode) {
+    super(midiEngine, "QWERTY (" + (mode == APC ? "APC" : "Key") + "  Mode)");
+    this.mode = mode;
+    if (mode == APC) {
+      mapAPC();
+    } else {
+      mapKeys();
+    }
+    registerKeyEvent(this);    
+  }
+
+  private void mapAPC() {
+    mapNote('1', 0, 53);
+    mapNote('2', 1, 53);
+    mapNote('3', 2, 53);
+    mapNote('4', 3, 53);
+    mapNote('5', 4, 53);
+    mapNote('6', 5, 53);
+    mapNote('q', 0, 54);
+    mapNote('w', 1, 54);
+    mapNote('e', 2, 54);
+    mapNote('r', 3, 54);
+    mapNote('t', 4, 54);
+    mapNote('y', 5, 54);
+    mapNote('a', 0, 55);
+    mapNote('s', 1, 55);
+    mapNote('d', 2, 55);
+    mapNote('f', 3, 55);
+    mapNote('g', 4, 55);
+    mapNote('h', 5, 55);
+    mapNote('z', 0, 56);
+    mapNote('x', 1, 56);
+    mapNote('c', 2, 56);
+    mapNote('v', 3, 56);
+    mapNote('b', 4, 56);
+    mapNote('n', 5, 56);
+  }
+
+  private void mapKeys() {
+    int note = 48;
+    mapNote('a', 1, note++);
+    mapNote('w', 1, note++);
+    mapNote('s', 1, note++);
+    mapNote('e', 1, note++);
+    mapNote('d', 1, note++);
+    mapNote('f', 1, note++);
+    mapNote('t', 1, note++);
+    mapNote('g', 1, note++);
+    mapNote('y', 1, note++);
+    mapNote('h', 1, note++);
+    mapNote('u', 1, note++);
+    mapNote('j', 1, note++);
+    mapNote('k', 1, note++);
+    mapNote('o', 1, note++);
+    mapNote('l', 1, note++);
+  }
+
+  void mapNote(char ch, int channel, int number) {
+    keyToNote.put(ch, new NoteMeta(channel, number));
+  }
+  
+  public void keyEvent(KeyEvent e) {
+    if (!enabled) {
+      return;
+    }
+    char c = Character.toLowerCase(e.getKeyChar());
+    NoteMeta nm = keyToNote.get(c);
+    if (nm != null) {
+      switch (e.getID()) {
+      case KeyEvent.KEY_PRESSED:
+        noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
+        break;
+      case KeyEvent.KEY_RELEASED:
+        noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
+        break;
+      }
+    }
+    if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
+      switch (c) {
+      case 'z':
+        octaveShift = constrain(octaveShift-1, -4, 4);
+        break;
+      case 'x':
+        octaveShift = constrain(octaveShift+1, -4, 4);
+        break;
+      }
+    }
+  }
+}
+
+public class GenericDeviceMidiInput extends SCMidiInput {
+  GenericDeviceMidiInput(MidiEngine midiEngine, MidiInputDevice d) {
+    super(midiEngine, d.getName().replace("Unknown vendor",""));
+    d.createInput(this);
+  }
+}
+
+public class APC40MidiInput extends GenericDeviceMidiInput {
 
   private boolean shiftOn = false;
   private LXEffect releaseEffect = null;
+  final private Engine.Deck targetDeck;
   
   APC40MidiInput(MidiEngine midiEngine, MidiInputDevice d) {
+    this(midiEngine, d, -1);
+  }
+  
+  APC40MidiInput(MidiEngine midiEngine, MidiInputDevice d, int deckIndex) {
     super(midiEngine, d);
+    targetDeck = (deckIndex < 0) ? null : lx.engine.getDecks().get(deckIndex);
+  }
+  
+  protected Engine.Deck getTargetDeck() {
+    if (targetDeck != null) {
+      return targetDeck;
+    }
+    return midiEngine.getFocusedDeck();
+  }
+  
+  protected SCPattern getTargetPattern() {
+    if (targetDeck != null) {
+      return (SCPattern) (targetDeck.getActivePattern());
+    }
+    return super.getTargetPattern();
   }
 
   private class GridPosition {
@@ -346,7 +390,6 @@ public class APC40MidiInput extends SCMidiInput {
     int pitch = note.getPitch();
     if (channel < 8) {
       if (pitch >= 53 && pitch <=57) return new GridPosition(pitch-53, channel);
-      else if (pitch == 52) return new GridPosition(5, channel);
     }
     return null;
   }
@@ -368,11 +411,39 @@ public class APC40MidiInput extends SCMidiInput {
   }
 
   protected void handleControllerChange(rwmidi.Controller cc) {
+    int channel = cc.getChannel();
     int number = cc.getCC();
+    float value = cc.getValue() / 127.;
     switch (number) {
+      
+    case 7:
+     switch (channel) {
+       case 0:
+         EFF_colorFucker.hueShift.setValue(value);
+         break;
+       case 1:
+         EFF_colorFucker.desat.setValue(value);
+         break;
+       case 2:
+         EFF_colorFucker.sharp.setValue(value);
+         break;
+       case 3:
+         EFF_blur.amount.setValue(value);
+         break;
+       case 4:
+         EFF_quantize.amount.setValue(value);
+         break;
+     }
+     break;
+     
+    // Master bright
+    case 14:
+      EFF_colorFucker.level.setValue(value);
+      break;
+
     // Crossfader
     case 15:
-      lx.engine.getDeck(1).getCrossfader().setValue(cc.getValue() / 127.);
+      lx.engine.getDeck(1).getCrossfader().setValue(value);
       break;
     }
     
@@ -383,9 +454,9 @@ public class APC40MidiInput extends SCMidiInput {
       parameterIndex = 8 + (number-16);
     }
     if (parameterIndex >= 0) {
-      List<LXParameter> parameters = midiEngine.getFocusedPattern().getParameters();
+      List<LXParameter> parameters = getTargetPattern().getParameters();
       if (parameterIndex < parameters.size()) {
-        parameters.get(parameterIndex).setValue(cc.getValue() / 127.);
+        parameters.get(parameterIndex).setValue(value);
       }
     }
     
@@ -393,7 +464,7 @@ public class APC40MidiInput extends SCMidiInput {
       int effectIndex = number - 20;
       List<LXParameter> parameters = glucose.getSelectedEffect().getParameters();
       if (effectIndex < parameters.size()) {
-        parameters.get(effectIndex).setValue(cc.getValue() / 127.);
+        parameters.get(effectIndex).setValue(value);
       }
     }
   }
@@ -405,9 +476,24 @@ public class APC40MidiInput extends SCMidiInput {
   }
 
   protected void handleNoteOn(Note note) {
-    int nPitch = note.getPitch(), nChan = note.getChannel();
+    int nPitch = note.getPitch();
+    int nChan = note.getChannel();
     switch (nPitch) {
-               
+    
+    case 49: // SOLO/CUE
+      switch (nChan) {
+        case 4:
+          EFF_colorFucker.mono.setValue(1);
+          break;
+        case 5:
+          EFF_colorFucker.invert.setValue(1);
+          break;
+        case 6:
+          lx.cycleBaseHue(60000);
+          break;
+      }
+      break;
+            
     case 82: // scene 1
       EFF_boom.trigger();
       break;
@@ -436,14 +522,14 @@ public class APC40MidiInput extends SCMidiInput {
       if (shiftOn) {
         glucose.incrementSelectedEffectBy(-1);
       } else {
-        midiEngine.getFocusedDeck().goPrev();
+        getTargetDeck().goPrev();
       }
       break;
     case 95: // down bank
       if (shiftOn) {
         glucose.incrementSelectedEffectBy(1);
       } else {
-        midiEngine.getFocusedDeck().goNext();
+        getTargetDeck().goNext();
       }
       break;
 
@@ -477,8 +563,35 @@ public class APC40MidiInput extends SCMidiInput {
   }
 
   protected void handleNoteOff(Note note) {
-    int nPitch = note.getPitch(), nChan = note.getChannel();
+    int nPitch = note.getPitch();
+    int nChan = note.getChannel();
+
     switch (nPitch) {
+      
+    case 49: // SOLO/CUE
+      switch (nChan) {
+        case 4:
+          EFF_colorFucker.mono.setValue(0);
+          break;
+        case 5:
+          EFF_colorFucker.invert.setValue(0);
+          break;
+        case 6:
+          lx.setBaseHue(lx.getBaseHue());
+          break;
+      }
+      break;
+
+    case 52: // CLIP STOP
+      if (nChan < PresetManager.NUM_PRESETS) {
+        if (shiftOn) {
+          presetManager.store(nChan);
+        } else {
+          presetManager.select(nChan);
+        }
+      }
+      break;
+
     case 90: // SEND C
       long tapDelta = millis() - tap1;
       if (lbtwn(tapDelta,5000,300*1000)) {     // hackish tapping mechanism
@@ -507,7 +620,7 @@ public class APC40MidiInput extends SCMidiInput {
   }
 }
 
-class KorgNanoKontrolMidiInput extends SCMidiInput {
+class KorgNanoKontrolMidiInput extends GenericDeviceMidiInput {
   
   KorgNanoKontrolMidiInput(MidiEngine midiEngine, MidiInputDevice d) {
     super(midiEngine, d);
@@ -552,15 +665,24 @@ class APC40MidiOutput implements LXParameter.Listener, GridOutput {
   private final MidiOutput output;
   private LXPattern focusedPattern = null;
   private LXEffect focusedEffect = null;
+  private final Engine.Deck targetDeck;
   
   APC40MidiOutput(MidiEngine midiEngine, MidiOutputDevice device) {
+    this(midiEngine, device, -1);
+  }
+  
+  APC40MidiOutput(MidiEngine midiEngine, MidiOutputDevice device, int deckIndex) {
     this.midiEngine = midiEngine;
     output = device.createOutput();
-    midiEngine.addListener(new MidiEngineListener() {
-      public void onFocusedDeck(int deckIndex) {
-        resetPatternParameters();
-      }
-    });
+    targetDeck = (deckIndex < 0) ? null : lx.engine.getDecks().get(deckIndex);
+    setDPatternOutputs();
+    if (targetDeck != null) {
+      midiEngine.addListener(new MidiEngineListener() {
+        public void onFocusedDeck(int deckIndex) {
+          resetPatternParameters();
+        }
+      });
+    }
     glucose.addEffectListener(new GLucose.EffectListener() {
       public void effectSelected(LXEffect effect) {
         resetEffectParameters();
@@ -572,10 +694,61 @@ class APC40MidiOutput implements LXParameter.Listener, GridOutput {
       }
     };
     for (Engine.Deck d : lx.engine.getDecks()) {
-      d.addListener(deckListener);
+      if (targetDeck == null || d == targetDeck) {
+        d.addListener(deckListener);
+      }
     }
+    presetManager.addListener(new PresetListener() {
+      public void onPresetLoaded(Preset preset) {
+        for (int i = 0; i < 8; ++i) {
+          output.sendNoteOn(i, 52, (preset.index == i) ? 1 : 0);
+        }
+      }
+      public void onPresetDirty(Preset preset) {
+        output.sendNoteOn(preset.index, 52, 2);
+      }
+      public void onPresetStored(Preset preset) {
+        onPresetLoaded(preset);
+      }
+      public void onPresetUnloaded() {
+        for (int i = 0; i < 8; ++i) {
+          output.sendNoteOn(i, 52, 0);
+        }
+      }
+    });
     resetParameters();
     midiEngine.grid.addOutput(this);
+
+    lx.cycleBaseHue(60000);
+    output.sendNoteOn(6, 49, 127);
+    
+    // Turn off the track selection lights and preset selectors
+    for (int i = 0; i < 8; ++i) {
+      output.sendNoteOn(i, 51, 0);
+      output.sendNoteOn(i, 52, 0);
+    }
+    
+    // Turn off the MASTER selector
+    output.sendNoteOn(0, 80, 0);
+  }
+  
+  private void setDPatternOutputs() {
+    for (Engine.Deck deck : lx.engine.getDecks()) {
+      if (targetDeck == null || deck == targetDeck) {
+        for (LXPattern pattern : deck.getPatterns()) {
+          if (pattern instanceof DPat) {
+            ((DPat)pattern).setAPCOutput(output);
+          }
+        }
+      }
+    }
+  }
+  
+  protected Engine.Deck getTargetDeck() {
+    if (targetDeck != null) {
+      return targetDeck;
+    }
+    return midiEngine.getFocusedDeck();
   }
 
   private void resetParameters() {
@@ -584,7 +757,7 @@ class APC40MidiOutput implements LXParameter.Listener, GridOutput {
   }
   
   private void resetPatternParameters() {
-    LXPattern newPattern = midiEngine.getFocusedDeck().getActivePattern();
+    LXPattern newPattern = getTargetDeck().getActivePattern();
     if (newPattern == focusedPattern) {
       return;
     }
@@ -602,9 +775,16 @@ class APC40MidiOutput implements LXParameter.Listener, GridOutput {
     while (i < 12) {
       sendKnob(i++, 0);
     }
-    for (int row = 0; row < 7; ++row) {
-      for (int col = 0; col < 8; ++col) {
-        setGridState(row, col, 0);
+    if (focusedPattern instanceof DPat) {
+      ((DPat)focusedPattern).updateLights();
+    } else {
+      for (int j = 0; j < 8; ++j) {
+        output.sendNoteOn(j, 48, 0);
+      }
+      for (int row = 0; row < 7; ++row) {
+        for (int col = 0; col < 8; ++col) {
+          setGridState(row, col, 0);
+        }
       }
     }
   }
@@ -662,9 +842,8 @@ class APC40MidiOutput implements LXParameter.Listener, GridOutput {
   }
   
   public void setGridState(int row, int col, int state) {
-    if (col < 8) {
-      if (row < 5) output.sendNoteOn(col, 53+row, state);
-      else if (row == 6) output.sendNoteOn(col, 52, state);
+    if (col < 8 && row < 5) {
+      output.sendNoteOn(col, 53+row, state);
     }
   }
 }