├── .github └── workflows │ ├── maven-check.yml │ └── maven.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ └── codeStyleConfig.xml ├── compiler.xml ├── dictionaries │ └── Bobho.xml ├── misc.xml ├── uiDesigner.xml └── vcs.xml ├── README.md ├── launchpad-x.iml ├── pom.xml └── src └── main ├── java └── io │ └── github │ └── jengamon │ └── novation │ ├── LaunchpadXExtension.java │ ├── LaunchpadXExtensionDefinition.java │ ├── Mode.java │ ├── ModeMachine.java │ ├── Utils.java │ ├── internal │ ├── ChannelType.java │ ├── HostErrorOutputStream.java │ ├── HostOutputStream.java │ └── Session.java │ ├── modes │ ├── AbstractMode.java │ ├── DrumPadMode.java │ ├── SessionMode.java │ ├── mixer │ │ ├── AbstractFaderMixerMode.java │ │ ├── AbstractMixerMode.java │ │ ├── AbstractSessionMixerMode.java │ │ ├── ControlsMixer.java │ │ ├── MuteMixer.java │ │ ├── PanMixer.java │ │ ├── RecordArmMixer.java │ │ ├── SendMixer.java │ │ ├── SoloMixer.java │ │ ├── StopClipMixer.java │ │ └── VolumeMixer.java │ └── session │ │ ├── ArrowPadLight.java │ │ ├── SessionPadLight.java │ │ └── TrackColorFaderLight.java │ └── surface │ ├── CCButton.java │ ├── Fader.java │ ├── LaunchpadXPad.java │ ├── LaunchpadXSurface.java │ ├── NoteButton.java │ └── state │ ├── FaderLightState.java │ └── PadLightState.java └── resources └── META-INF └── services └── com.bitwig.extension.ExtensionDefinition /.github/workflows/maven-check.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up JDK 13 | uses: actions/setup-java@v2 14 | with: 15 | distribution: 'temurin' 16 | java-version: '21.0.4+7.0.LTS' 17 | - name: Build with Maven 18 | run: mvn -B install --file pom.xml 19 | - uses: actions/upload-artifact@v4 20 | with: 21 | name: Launchpad Extension 22 | path: ./target/LaunchpadX.bwextension 23 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up JDK 15 | uses: actions/setup-java@v2 16 | with: 17 | distribution: 'temurin' 18 | java-version: '21.0.4+7.0.LTS' 19 | - name: Build with Maven 20 | run: mvn -B install --file pom.xml 21 | - name: Upload asset to release 22 | uses: Shopify/upload-to-release@v2.0.0 23 | with: 24 | name: 'LaunchpadX.bwextension' 25 | path: './target/LaunchpadX.bwextension' 26 | repo-token: ${{ secrets.GITHUB_TOKEN }} 27 | content-type: 'application/java-archive' 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.idea/ 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | launchpad-x -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/dictionaries/Bobho.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | aftertouch 5 | fader 6 | novation 7 | sysex 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jengamon's Launchpad X for Bitwig 2 | 3 | ![Build status badge](https://github.com/Jengamon/Launchpad-X-Bitwig-Script/workflows/Java%20CI/badge.svg) 4 | 5 | ## Why not DrivenByMoss? 6 | 7 | I love the work that Moss has done, and I would switch between the two scripts. 8 | It's just that I wanted a more tactile version of the script, that more 9 | took advantage for the hardware's features rather than replace them. 10 | 11 | Basically, I wanted it to work almost exactly like it does in the manual. 12 | 13 | There are some changes, which I probably should document, but nahhh for now. 14 | 15 | This script is much cleaner than my Mini Mk3 script. I might port over the Mk3 16 | to this framework, but we'll see. 17 | 18 | ## TODO 19 | 20 | - Fix release CI so that the bwextension is uploaded again 21 | 22 | ## Installation 23 | 24 | Simply download the desired version of "LaunchpadX.bwextension" from the Releases page, 25 | then put it in your Bitwig "Extensions" folder. 26 | 27 | Or you can build it yourself (and at the bleeding edge) by downloading the repository 28 | and running "mvn install" in the root directory with both JDK (at least 12) and 29 | Maven installed. 30 | -------------------------------------------------------------------------------- /launchpad-x.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | io.github.jengamon.novation 6 | launchpad-x 7 | jar 8 | Launchpad X 9 | 1.2 10 | 11 | 12 | UTF-8 13 | 14 | 15 | 16 | 17 | bitwig 18 | Bitwig Maven Repository 19 | https://maven.bitwig.com 20 | 21 | 22 | 23 | 24 | 25 | com.bitwig 26 | extension-api 27 | 18 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | com.coderplus.maven.plugins 36 | copy-rename-maven-plugin 37 | 1.0 38 | 39 | 40 | rename-file 41 | install 42 | 43 | copy 44 | 45 | 46 | ${project.build.directory}/${project.build.finalName}.jar 47 | ${project.build.directory}/LaunchpadX.bwextension 48 | 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-compiler-plugin 55 | 3.8.0 56 | 57 | 58 | compile 59 | compile 60 | 61 | compile 62 | 63 | 64 | 65 | testCompile 66 | test-compile 67 | 68 | testCompile 69 | 70 | 71 | 72 | 73 | true 74 | true 75 | 1.8 76 | 1.8 77 | UTF-8 78 | 1024m 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/LaunchpadXExtension.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation; 2 | 3 | import com.bitwig.extension.api.util.midi.ShortMidiMessage; 4 | import com.bitwig.extension.callback.ObjectValueChangedCallback; 5 | import com.bitwig.extension.controller.ControllerExtension; 6 | import com.bitwig.extension.controller.api.*; 7 | import io.github.jengamon.novation.internal.ChannelType; 8 | import io.github.jengamon.novation.internal.HostErrorOutputStream; 9 | import io.github.jengamon.novation.internal.HostOutputStream; 10 | import io.github.jengamon.novation.internal.Session; 11 | import io.github.jengamon.novation.modes.AbstractMode; 12 | import io.github.jengamon.novation.modes.DrumPadMode; 13 | import io.github.jengamon.novation.modes.SessionMode; 14 | import io.github.jengamon.novation.modes.mixer.*; 15 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 16 | import io.github.jengamon.novation.surface.state.PadLightState; 17 | 18 | import java.io.PrintStream; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.concurrent.atomic.AtomicBoolean; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | public class LaunchpadXExtension extends ControllerExtension { 25 | private Session mSession; 26 | private HardwareSurface mSurface; 27 | private LaunchpadXSurface mLSurface; 28 | private ModeMachine mMachine; 29 | 30 | private final static String CLIP_LAUNCHER = "Clip Launcher"; 31 | private final static String GLOBAL = "Global"; 32 | private final static String TOGGLE_RECORD = "Toggle Record"; 33 | private final static String CYCLE_TRACKS = "Cycle Tracks"; 34 | private final static String LAUNCH_ALT = "Launch Alt"; 35 | 36 | protected LaunchpadXExtension(final LaunchpadXExtensionDefinition definition, final ControllerHost host) { 37 | super(definition, host); 38 | } 39 | 40 | @Override 41 | public void init() { 42 | final ControllerHost host = getHost(); 43 | 44 | Preferences prefs = host.getPreferences(); 45 | DocumentState documentPrefs = host.getDocumentState(); 46 | BooleanValue mSwapOnBoot = prefs.getBooleanSetting("Swap to Session on Boot?", "Behavior", true); 47 | BooleanValue mPulseSessionPads = prefs.getBooleanSetting("Pulse Session Scene Pads?", "Behavior", false); 48 | // BooleanValue mFollowCursorTrack = prefs.getBooleanSetting("Follow Cursor Track?", "Behavior", true); 49 | BooleanValue mViewableBanks = prefs.getBooleanSetting("Viewable Bank?", "Behavior", true); 50 | BooleanValue mStopClipsBeforeToggle = prefs.getBooleanSetting("Stop Recording Clips before Toggle Record?", "Record Button", false); 51 | 52 | EnumValue mRecordLevel = documentPrefs.getEnumSetting("Rec. Target", "Record Button", new String[]{GLOBAL, CLIP_LAUNCHER}, CLIP_LAUNCHER); 53 | EnumValue mRecordAction = documentPrefs.getEnumSetting("Action", "Record Button", new String[]{TOGGLE_RECORD, CYCLE_TRACKS, LAUNCH_ALT}, TOGGLE_RECORD); 54 | 55 | // Replace System.out and System.err with ones that should actually work 56 | System.setOut(new PrintStream(new HostOutputStream(host))); 57 | System.setErr(new PrintStream(new HostErrorOutputStream(host))); 58 | 59 | // Create the requisite state objects 60 | mSession = new Session(host); 61 | mSurface = host.createHardwareSurface(); 62 | Transport mTransport = host.createTransport(); 63 | CursorTrack mCursorTrack = host.createCursorTrack(8, 0); 64 | CursorDevice mCursorDevice = mCursorTrack.createCursorDevice("Primary", "Primary Instrument", 0, CursorDeviceFollowMode.FIRST_INSTRUMENT); 65 | CursorDevice mControlsCursorDevice = mCursorTrack.createCursorDevice("Primary IoE", "Primary Device", 0, CursorDeviceFollowMode.FOLLOW_SELECTION); 66 | TrackBank mSessionTrackBank = host.createTrackBank(8, 0, 8, true); 67 | mSessionTrackBank.setSkipDisabledItems(true); 68 | 69 | mViewableBanks.addValueObserver(vb -> mSessionTrackBank.sceneBank().setIndication(vb)); 70 | 71 | mCursorTrack.playingNotes().addValueObserver(new ObjectValueChangedCallback() { 72 | // yoinked from the BW script mwahahaha 73 | @Override 74 | public void valueChanged(PlayingNote[] playingNotes) { 75 | for (int pitch : mPrevPitches) { 76 | mSession.midiOut(ChannelType.DAW).sendMidi(0x8f, pitch, 0); 77 | } 78 | 79 | mPrevPitches.clear(); 80 | 81 | for (PlayingNote playingNote : playingNotes) { 82 | mSession.midiOut(ChannelType.DAW).sendMidi(0x9f, playingNote.pitch(), 21); 83 | mPrevPitches.add(playingNote.pitch()); 84 | } 85 | } 86 | 87 | final ArrayList mPrevPitches = new ArrayList<>(); 88 | }); 89 | 90 | // Create surface buttons and their lights 91 | mSurface.setPhysicalSize(241, 241); 92 | mLSurface = new LaunchpadXSurface(host, mSession, mSurface); 93 | mMachine = new ModeMachine(mSession); 94 | 95 | 96 | // Setup "launchAlt" var 97 | AtomicBoolean launchAlt = new AtomicBoolean(false); 98 | AtomicBoolean launchAltConfig = new AtomicBoolean(false); 99 | 100 | // Session modes 101 | mMachine.register(Mode.SESSION, new SessionMode(mSessionTrackBank, mTransport, mLSurface, host, mPulseSessionPads, launchAlt)); 102 | mMachine.register(Mode.DRUM, new DrumPadMode(host, mSession, mLSurface, mCursorDevice)); 103 | mMachine.register(Mode.UNKNOWN, new AbstractMode() { 104 | @Override 105 | public List onBind(LaunchpadXSurface surface) { 106 | return new ArrayList<>(); 107 | } 108 | }); 109 | // Mixer modes 110 | AtomicReference mixerMode = new AtomicReference<>(Mode.MIXER_VOLUME); 111 | mMachine.register(Mode.MIXER_VOLUME, new VolumeMixer(mixerMode, host, mTransport, mLSurface, mSessionTrackBank)); 112 | mMachine.register(Mode.MIXER_PAN, new PanMixer(mixerMode, host, mTransport, mLSurface, mSessionTrackBank)); 113 | mMachine.register(Mode.MIXER_SEND, new SendMixer(mixerMode, host, mTransport, mLSurface, mCursorTrack)); 114 | mMachine.register(Mode.MIXER_CONTROLS, new ControlsMixer(mixerMode, host, mTransport, mLSurface, mControlsCursorDevice)); 115 | mMachine.register(Mode.MIXER_STOP, new StopClipMixer(mixerMode, host, mTransport, mLSurface, mSessionTrackBank, launchAlt)); 116 | mMachine.register(Mode.MIXER_MUTE, new MuteMixer(mixerMode, host, mTransport, mLSurface, mSessionTrackBank, launchAlt)); 117 | mMachine.register(Mode.MIXER_SOLO, new SoloMixer(mixerMode, host, mTransport, mLSurface, mSessionTrackBank, launchAlt)); 118 | mMachine.register(Mode.MIXER_ARM, new RecordArmMixer(mixerMode, host, mTransport, mLSurface, mSessionTrackBank, launchAlt)); 119 | // Select record button behavior and light it accordingly 120 | mCursorTrack.hasNext().markInterested(); 121 | AtomicBoolean recordActionToggle = new AtomicBoolean(false); 122 | AtomicBoolean recordLevelGlobal = new AtomicBoolean(false); 123 | mRecordAction.addValueObserver(val -> { 124 | recordActionToggle.set(val.equals(TOGGLE_RECORD)); 125 | launchAltConfig.set(val.equals(LAUNCH_ALT)); 126 | }); 127 | mRecordLevel.addValueObserver(val -> recordLevelGlobal.set(val.equals("Global"))); 128 | 129 | ClipLauncherSlotBank[] clsBanks = new ClipLauncherSlotBank[8]; 130 | for (int i = 0; i < mSessionTrackBank.getSizeOfBank(); i++) { 131 | Track track = mSessionTrackBank.getItemAt(i); 132 | ClipLauncherSlotBank slotbank = track.clipLauncherSlotBank(); 133 | clsBanks[i] = slotbank; 134 | for (int j = 0; j < slotbank.getSizeOfBank(); j++) { 135 | ClipLauncherSlot slot = slotbank.getItemAt(j); 136 | slot.isRecording().markInterested(); 137 | } 138 | } 139 | 140 | Runnable selectAction = () -> { 141 | if (recordActionToggle.get()) { 142 | boolean clipStopped = false; 143 | 144 | if (mStopClipsBeforeToggle.get()) { 145 | for (ClipLauncherSlotBank bank : clsBanks) { 146 | int targetSlot = -1; 147 | for (int i = 0; i < bank.getSizeOfBank(); i++) { 148 | ClipLauncherSlot slot = bank.getItemAt(i); 149 | if (slot.isRecording().get()) { 150 | targetSlot = i; 151 | break; 152 | } 153 | } 154 | 155 | if (targetSlot >= 0) { 156 | clipStopped = true; 157 | bank.stop(); 158 | bank.launch(targetSlot); 159 | } 160 | } 161 | } 162 | 163 | // Only toggle the record button if we *didn't* stop any clips. 164 | if (!clipStopped) { 165 | if (recordLevelGlobal.get()) { 166 | mTransport.isArrangerRecordEnabled().toggle(); 167 | } else { 168 | mTransport.isClipLauncherOverdubEnabled().toggle(); 169 | } 170 | } 171 | } else if (!launchAltConfig.get()) { 172 | // if we *aren't* configured to alt launch 173 | if (mCursorTrack.hasNext().get()) { 174 | mCursorTrack.selectNext(); 175 | } else { 176 | mCursorTrack.selectFirst(); 177 | } 178 | } else { 179 | launchAlt.set(true); 180 | } 181 | host.requestFlush(); 182 | }; 183 | 184 | HardwareActionBindable recordState = host.createAction(selectAction, () -> "Press Record Button"); 185 | mLSurface.record().button().pressedAction().setBinding(recordState); 186 | mLSurface.record().button().releasedAction().setBinding(host.createAction( 187 | () -> { 188 | if (launchAltConfig.get()) { 189 | launchAlt.set(false); 190 | } 191 | host.requestFlush(); 192 | }, () -> "Release Record Action" 193 | )); 194 | 195 | MultiStateHardwareLight recordLight = mLSurface.record().light(); 196 | BooleanValue arrangerRecord = mTransport.isArrangerRecordEnabled(); 197 | BooleanValue clipLauncherOverdub = mTransport.isClipLauncherOverdubEnabled(); 198 | mRecordLevel.addValueObserver( 199 | target -> { 200 | if(recordActionToggle.get()) { 201 | if (target.equals(GLOBAL)) { 202 | if (arrangerRecord.get()) { 203 | recordLight.state().setValue(PadLightState.solidLight(5)); 204 | } else { 205 | recordLight.state().setValue(PadLightState.solidLight(7)); 206 | } 207 | } else if (target.equals(CLIP_LAUNCHER)) { 208 | if (clipLauncherOverdub.get()) { 209 | recordLight.state().setValue(PadLightState.solidLight(5)); 210 | } else { 211 | recordLight.state().setValue(PadLightState.solidLight(7)); 212 | } 213 | } 214 | } 215 | } 216 | ); 217 | arrangerRecord.addValueObserver(are -> { 218 | if (recordActionToggle.get() && mRecordLevel.get().equals(GLOBAL)) { 219 | if (are) { 220 | recordLight.state().setValue(PadLightState.solidLight(5)); 221 | } else { 222 | recordLight.state().setValue(PadLightState.solidLight(7)); 223 | } 224 | } 225 | }); 226 | clipLauncherOverdub.addValueObserver(ode -> { 227 | if (recordActionToggle.get() && mRecordLevel.get().equals(CLIP_LAUNCHER)) { 228 | if (ode) { 229 | recordLight.state().setValue(PadLightState.solidLight(5)); 230 | } else { 231 | recordLight.state().setValue(PadLightState.solidLight(7)); 232 | } 233 | } 234 | }); 235 | mRecordAction.addValueObserver(val -> { 236 | if (val.equals(CYCLE_TRACKS)) { 237 | // cycle tracks lighting 238 | recordLight.state().setValue(PadLightState.solidLight(13)); 239 | } else if (val.equals(LAUNCH_ALT)) { 240 | recordLight.state().setValue(PadLightState.solidLight(3)); 241 | } else { 242 | // Record action 243 | if ((arrangerRecord.get() && mRecordLevel.get().equals(GLOBAL)) || (clipLauncherOverdub.get() && mRecordLevel.get().equals(CLIP_LAUNCHER))) { 244 | recordLight.state().setValue(PadLightState.solidLight(5)); 245 | } else { 246 | recordLight.state().setValue(PadLightState.solidLight(7)); 247 | } 248 | } 249 | }); 250 | 251 | mLSurface.novation().light().state().setValue(PadLightState.solidLight(3)); 252 | 253 | AtomicReference lastSessionMode = new AtomicReference<>(Mode.SESSION); 254 | HardwareActionBindable mSessionAction = host.createAction(() -> { 255 | switch (mMachine.mode()) { 256 | case SESSION: 257 | lastSessionMode.set(mixerMode.get()); 258 | mMachine.setMode(mLSurface, mixerMode.get()); 259 | break; 260 | case MIXER_VOLUME: 261 | case MIXER_PAN: 262 | case MIXER_SEND: 263 | case MIXER_CONTROLS: 264 | case MIXER_STOP: 265 | case MIXER_MUTE: 266 | case MIXER_SOLO: 267 | case MIXER_ARM: 268 | lastSessionMode.set(Mode.SESSION); 269 | mMachine.setMode(mLSurface, Mode.SESSION); 270 | break; 271 | case DRUM: 272 | case UNKNOWN: 273 | mMachine.setMode(mLSurface, lastSessionMode.get()); 274 | break; 275 | default: 276 | throw new RuntimeException("Unknown mode " + mMachine.mode()); 277 | } 278 | }, () -> "Press Session View"); 279 | 280 | HardwareActionBindable mNoteAction = host.createAction(() -> { 281 | Mode om = mMachine.mode(); 282 | if (om != Mode.DRUM && om != Mode.UNKNOWN) { 283 | lastSessionMode.set(om); 284 | } 285 | mSession.sendSysex("00 01"); 286 | mMachine.setMode(mLSurface, Mode.DRUM); 287 | }, () -> "Press Note View"); 288 | 289 | HardwareActionBindable mCustomAction = host.createAction(() -> { 290 | Mode om = mMachine.mode(); 291 | if (om != Mode.DRUM && om != Mode.UNKNOWN) { 292 | lastSessionMode.set(om); 293 | } 294 | mMachine.setMode(mLSurface, Mode.UNKNOWN); 295 | }, () -> "Press Custom View"); 296 | 297 | if (mSwapOnBoot.get()) { 298 | mSessionAction.invoke(); 299 | } else { 300 | mMachine.setMode(mLSurface, Mode.DRUM); 301 | } 302 | 303 | mSessionAction.addBinding(mLSurface.session().button().pressedAction()); 304 | mNoteAction.addBinding(mLSurface.note().button().pressedAction()); 305 | mCustomAction.addBinding(mLSurface.custom().button().pressedAction()); 306 | 307 | mSession.setMidiCallback(ChannelType.DAW, this::onMidi0); 308 | mSession.setSysexCallback(ChannelType.DAW, this::onSysex0); 309 | mSession.setMidiCallback(ChannelType.CUSTOM, this::onMidi1); 310 | 311 | System.out.println("Launchpad X Initialized"); 312 | 313 | host.requestFlush(); 314 | } 315 | 316 | @Override 317 | public void exit() { 318 | mSession.shutdown(); 319 | System.out.println("Launchpad X Exited"); 320 | } 321 | 322 | @Override 323 | public void flush() { 324 | mSurface.updateHardware(); 325 | } 326 | 327 | /** 328 | * Called when we receive short MIDI message on port 0. 329 | */ 330 | private void onMidi0(ShortMidiMessage msg) { 331 | // mSurface.invalidateHardwareOutputState(); 332 | // System.out.println(msg); 333 | } 334 | 335 | /** 336 | * Called when we receive sysex MIDI message on port 0. 337 | */ 338 | private void onSysex0(final String data) { 339 | byte[] sysex = Utils.parseSysex(data); 340 | mMachine.sendSysex(sysex); 341 | mSurface.invalidateHardwareOutputState(); 342 | } 343 | 344 | /** 345 | * Called when we receive short MIDI message on port 1. 346 | */ 347 | private void onMidi1(ShortMidiMessage msg) { 348 | // System.out.println("C: " + Utils.toHexString((byte)msg.getStatusByte()) + Utils.toHexString((byte)msg.getData1()) + Utils.toHexString((byte)msg.getData2())); 349 | } 350 | 351 | // /** Called when we receive sysex MIDI message on port 1. */ 352 | // private void onSysex1(final String data) 353 | // { 354 | // } 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/LaunchpadXExtensionDefinition.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation; 2 | 3 | import com.bitwig.extension.api.PlatformType; 4 | import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; 5 | import com.bitwig.extension.controller.ControllerExtensionDefinition; 6 | import com.bitwig.extension.controller.api.ControllerHost; 7 | 8 | import java.util.UUID; 9 | 10 | public class LaunchpadXExtensionDefinition extends ControllerExtensionDefinition 11 | { 12 | private static final UUID DRIVER_ID = UUID.fromString("54555913-b867-4c61-8866-5e79ca63aa88"); 13 | 14 | public LaunchpadXExtensionDefinition() 15 | { 16 | } 17 | 18 | @Override 19 | public String getName() 20 | { 21 | return "Launchpad X"; 22 | } 23 | 24 | @Override 25 | public String getAuthor() 26 | { 27 | return "Jengamon"; 28 | } 29 | 30 | @Override 31 | public String getVersion() 32 | { 33 | return "1.2"; 34 | } 35 | 36 | @Override 37 | public UUID getId() 38 | { 39 | return DRIVER_ID; 40 | } 41 | 42 | @Override 43 | public String getHardwareVendor() 44 | { 45 | return "Novation"; 46 | } 47 | 48 | @Override 49 | public String getHardwareModel() 50 | { 51 | return "Launchpad X"; 52 | } 53 | 54 | @Override 55 | public int getRequiredAPIVersion() 56 | { 57 | return 18; 58 | } 59 | 60 | @Override 61 | public int getNumMidiInPorts() 62 | { 63 | return 2; 64 | } 65 | 66 | @Override 67 | public int getNumMidiOutPorts() 68 | { 69 | return 2; 70 | } 71 | 72 | @Override 73 | public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, final PlatformType platformType) 74 | { 75 | if (platformType == PlatformType.WINDOWS) 76 | { 77 | list.add(new String[]{"LPX MIDI", "MIDIIN2 (LPX MIDI)"}, new String[]{"LPX MIDI", "MIDIOUT2 (LPX MIDI)"}); 78 | } 79 | else if (platformType == PlatformType.MAC) 80 | { 81 | // TODO: Find a good guess for the Mac names. 82 | list.add(new String[]{"Launchpad X LPX DAW Out", "Launchpad X LPX MIDI Out"}, new String[]{"Launchpad X LPX DAW In", "Launchpad X LPX MIDI In"}); 83 | } 84 | else if (platformType == PlatformType.LINUX) 85 | { 86 | // TODO Find better guess. Get a Linux. 87 | list.add(new String[]{"Launchpad X MIDI 1", "Launchpad X MIDI 2"}, new String[]{"Launchpad X MIDI 1", "Launchpad X MIDI 2"}); 88 | } 89 | } 90 | 91 | @Override 92 | public LaunchpadXExtension createInstance(final ControllerHost host) 93 | { 94 | return new LaunchpadXExtension(this, host); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/Mode.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation; 2 | 3 | public enum Mode { 4 | SESSION, 5 | DRUM, 6 | 7 | // Mixer Submodes 8 | MIXER_VOLUME, 9 | MIXER_PAN, 10 | MIXER_SEND, 11 | MIXER_CONTROLS, 12 | MIXER_STOP, 13 | MIXER_MUTE, 14 | MIXER_SOLO, 15 | MIXER_ARM, 16 | 17 | UNKNOWN, 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/ModeMachine.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation; 2 | 3 | import com.bitwig.extension.controller.api.HardwareBinding; 4 | import io.github.jengamon.novation.internal.Session; 5 | import io.github.jengamon.novation.modes.AbstractMode; 6 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class ModeMachine { 14 | private final Map mModes; 15 | private Mode mMode; 16 | private AbstractMode mModus; 17 | private List mBindings; 18 | private final Session mSession; 19 | 20 | public ModeMachine(Session session) { 21 | mModes = new HashMap<>(); 22 | mBindings = new ArrayList<>(); 23 | mMode = Mode.UNKNOWN; 24 | mSession = session; 25 | } 26 | 27 | public Mode mode() { return mMode; } 28 | 29 | public void register(Mode mode, AbstractMode am) { 30 | am.onInit(this, mode); 31 | mModes.put(mode, am); 32 | } 33 | 34 | public void setMode(LaunchpadXSurface surface, Mode mode) { 35 | for(HardwareBinding binding : mBindings) { 36 | binding.removeBinding(); 37 | } 38 | mMode = mode; 39 | if(!mModes.containsKey(mode)) throw new RuntimeException("Invalid mode state: " + mode); 40 | surface.clear(); 41 | mModus = mModes.get(mode); 42 | mBindings = mModus.onBind(surface); 43 | mModus.finishedBind(mSession); 44 | redraw(surface); 45 | } 46 | 47 | public void redraw(LaunchpadXSurface surface) { 48 | mModus.onDraw(surface); 49 | } 50 | 51 | public void sendSysex(byte[] message) { 52 | List responses = mModes.get(mMode).processSysex(message); 53 | for(String response : responses) { 54 | mSession.sendSysex(response); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/Utils.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation; 2 | 3 | import com.bitwig.extension.api.Color; 4 | 5 | import java.util.*; 6 | import java.util.stream.Collectors; 7 | 8 | public class Utils { 9 | private static final Color[] NOVATION_COLORS = new Color[]{ 10 | // 00 - 07 11 | Color.fromRGBA255(0, 0, 0, 0), 12 | Color.fromRGB255(0x50, 0x50, 0x50), 13 | Color.fromRGB255(0xdd, 0xdd, 0xdd), 14 | Color.fromRGB255(0xff, 0xff, 0xff), 15 | Color.fromRGB255(0xf9, 0xb3, 0xb2), 16 | Color.fromRGB255(0xf5, 0x5f, 0x5e), 17 | Color.fromRGB255(0xd5, 0x60, 0x5f), 18 | Color.fromRGB255(0xad, 0x60, 0x60), 19 | // 08 - 0F 20 | Color.fromRGB255(0xfe, 0xf3, 0xd4), 21 | Color.fromRGB255(0xf9, 0xb2, 0x5a), 22 | Color.fromRGB255(0xd7, 0x8b, 0x5d), 23 | Color.fromRGB255(0xae, 0x76, 0x5f), 24 | Color.fromRGB255(0xfd, 0xee, 0x9d), 25 | Color.fromRGB255(0xfe, 0xff, 0x52), 26 | Color.fromRGB255(0xdc, 0xdd, 0x57), 27 | Color.fromRGB255(0xb2, 0xb3, 0x5c), 28 | // 10 - 17 29 | Color.fromRGB255(0xdf, 0xff, 0x9c), 30 | Color.fromRGB255(0xc7, 0xff, 0x54), 31 | Color.fromRGB255(0xa7, 0xdd, 0x59), 32 | Color.fromRGB255(0x86, 0xb3, 0x5d), 33 | Color.fromRGB255(0xc8, 0xff, 0xb0), 34 | Color.fromRGB255(0x79, 0xff, 0x56), 35 | Color.fromRGB255(0x72, 0xdd, 0x59), 36 | Color.fromRGB255(0x6b, 0xb3, 0x5d), 37 | // 18 - 1F 38 | Color.fromRGB255(0xc7, 0xfe, 0xbf), 39 | Color.fromRGB255(0x7a, 0xff, 0x87), 40 | Color.fromRGB255(0x73, 0xdd, 0x71), 41 | Color.fromRGB255(0x6b, 0xb3, 0x68), 42 | Color.fromRGB255(0xc8, 0xff, 0xca), 43 | Color.fromRGB255(0x7b, 0xff, 0xcb), 44 | Color.fromRGB255(0x72, 0xdb, 0x9e), 45 | Color.fromRGB255(0x6b, 0xb3, 0x7f), 46 | // 20 - 27 47 | Color.fromRGB255(0xc9, 0xff, 0xf3), 48 | Color.fromRGB255(0x7c, 0xff, 0xe9), 49 | Color.fromRGB255(0x74, 0xdd, 0xc2), 50 | Color.fromRGB255(0x6c, 0xb3, 0x95), 51 | Color.fromRGB255(0xc8, 0xf3, 0xff), 52 | Color.fromRGB255(0x79, 0xef, 0xff), 53 | Color.fromRGB255(0x71, 0xc7, 0xde), 54 | Color.fromRGB255(0x6a, 0xa1, 0xb4), 55 | // 28 - 2F 56 | Color.fromRGB255(0xc5, 0xdd, 0xff), 57 | Color.fromRGB255(0x72, 0xc8, 0xff), 58 | Color.fromRGB255(0x6b, 0xa2, 0xdf), 59 | Color.fromRGB255(0x66, 0x81, 0xb5), 60 | Color.fromRGB255(0xa1, 0x8d, 0xff), 61 | Color.fromRGB255(0x65, 0x63, 0xff), 62 | Color.fromRGB255(0x64, 0x62, 0xe0), 63 | Color.fromRGB255(0x63, 0x62, 0xb5), 64 | // 30 - 37 65 | Color.fromRGB255(0xcb, 0xb3, 0xff), 66 | Color.fromRGB255(0x9f, 0x62, 0xff), 67 | Color.fromRGB255(0x80, 0x62, 0xe0), 68 | Color.fromRGB255(0x75, 0x62, 0xb5), 69 | Color.fromRGB255(0xfa, 0xb3, 0xff), 70 | Color.fromRGB255(0xf7, 0x61, 0xff), 71 | Color.fromRGB255(0xd6, 0x61, 0xe0), 72 | Color.fromRGB255(0xae, 0x61, 0xb5), 73 | // 38 - 3F 74 | Color.fromRGB255(0xf9, 0xb3, 0xd6), 75 | Color.fromRGB255(0xf6, 0x60, 0xc3), 76 | Color.fromRGB255(0xd5, 0x60, 0xa2), 77 | Color.fromRGB255(0xae, 0x61, 0x8d), 78 | Color.fromRGB255(0xf6, 0x75, 0x5d), 79 | Color.fromRGB255(0xe4, 0xb2, 0x5a), 80 | Color.fromRGB255(0xda, 0xc2, 0x5a), 81 | Color.fromRGB255(0xa0, 0xa1, 0x5d), 82 | // 40 - 47 83 | Color.fromRGB255(0x6b, 0xb3, 0x5d), 84 | Color.fromRGB255(0x6c, 0xb3, 0x8b), 85 | Color.fromRGB255(0x68, 0x8d, 0xd7), 86 | Color.fromRGB255(0x65, 0x63, 0xff), 87 | Color.fromRGB255(0x6c, 0xb3, 0xb4), 88 | Color.fromRGB255(0x8b, 0x62, 0xf7), 89 | Color.fromRGB255(0xca, 0xb3, 0xc2), 90 | Color.fromRGB255(0x8a, 0x76, 0x81), 91 | // 48 - 4F 92 | Color.fromRGB255(0xf5, 0x5f, 0x5e), 93 | Color.fromRGB255(0xf3, 0xff, 0x9c), 94 | Color.fromRGB255(0xee, 0xfc, 0x53), 95 | Color.fromRGB255(0xd0, 0xff, 0x54), 96 | Color.fromRGB255(0x83, 0xdd, 0x59), 97 | Color.fromRGB255(0x7b, 0xff, 0xcb), 98 | Color.fromRGB255(0x78, 0xea, 0xff), 99 | Color.fromRGB255(0x6c, 0xa2, 0xff), 100 | // 50 - 57 101 | Color.fromRGB255(0x8b, 0x62, 0xff), 102 | Color.fromRGB255(0xc7, 0x62, 0xff), 103 | Color.fromRGB255(0xe8, 0x8c, 0xdf), 104 | Color.fromRGB255(0x9d, 0x76, 0x5f), 105 | Color.fromRGB255(0xf8, 0xa0, 0x5b), 106 | Color.fromRGB255(0xdf, 0xf9, 0x54), 107 | Color.fromRGB255(0xd8, 0xff, 0x85), 108 | Color.fromRGB255(0x79, 0xff, 0x56), 109 | // 58 - 5F 110 | Color.fromRGB255(0xbb, 0xff, 0x9d), 111 | Color.fromRGB255(0xd1, 0xfc, 0xd4), 112 | Color.fromRGB255(0xbc, 0xff, 0xf6), 113 | Color.fromRGB255(0xcf, 0xe4, 0xff), 114 | Color.fromRGB255(0xa5, 0xc2, 0xf8), 115 | Color.fromRGB255(0xd4, 0xc2, 0xfb), 116 | Color.fromRGB255(0xf2, 0x8c, 0xff), 117 | Color.fromRGB255(0xf6, 0x60, 0xce), 118 | // 60 - 67 119 | Color.fromRGB255(0xf9, 0xc1, 0x59), 120 | Color.fromRGB255(0xf1, 0xee, 0x55), 121 | Color.fromRGB255(0xe5, 0xff, 0x53), 122 | Color.fromRGB255(0xdb, 0xcc, 0x59), 123 | Color.fromRGB255(0xb1, 0xa1, 0x5d), 124 | Color.fromRGB255(0x6c, 0xba, 0x73), 125 | Color.fromRGB255(0x7f, 0xc2, 0x8a), 126 | Color.fromRGB255(0x81, 0x81, 0xa2), 127 | // 68 - 6F 128 | Color.fromRGB255(0x83, 0x8c, 0xce), 129 | Color.fromRGB255(0xc9, 0xaa, 0x7f), 130 | Color.fromRGB255(0xd5, 0x60, 0x5f), 131 | Color.fromRGB255(0xf3, 0xb3, 0x9f), 132 | Color.fromRGB255(0xf3, 0xb9, 0x71), 133 | Color.fromRGB255(0xfd, 0xf3, 0x85), 134 | Color.fromRGB255(0xea, 0xf9, 0x9c), 135 | Color.fromRGB255(0xd6, 0xee, 0x6e), 136 | // 70 - 77 137 | Color.fromRGB255(0x81, 0x81, 0xa2), 138 | Color.fromRGB255(0xf9, 0xf9, 0xd3), 139 | Color.fromRGB255(0xe0, 0xfc, 0xe3), 140 | Color.fromRGB255(0xe9, 0xe9, 0xff), 141 | Color.fromRGB255(0xe3, 0xd5, 0xff), 142 | Color.fromRGB255(0xb3, 0xb3, 0xb3), 143 | Color.fromRGB255(0xd5, 0xd5, 0xd5), 144 | Color.fromRGB255(0xfa, 0xff, 0xff), 145 | // 78 - 7F 146 | Color.fromRGB255(0xe0, 0x60, 0x5f), 147 | Color.fromRGB255(0xa5, 0x60, 0x60), 148 | Color.fromRGB255(0x8f, 0xf6, 0x56), 149 | Color.fromRGB255(0x6b, 0xb3, 0x5d), 150 | Color.fromRGB255(0xf1, 0xee, 0x55), 151 | Color.fromRGB255(0xb1, 0xa1, 0x5d), 152 | Color.fromRGB255(0xea, 0xc1, 0x59), 153 | Color.fromRGB255(0xbc, 0x75, 0x5f) 154 | }; 155 | 156 | public static String printColor(Color c) { 157 | return "R" + c.getRed255() + "G" + c.getGreen255() + "B" + c.getBlue255() + "A" + c.getAlpha255(); 158 | } 159 | 160 | public static Color fromNovation(byte i) { 161 | return NOVATION_COLORS[i]; 162 | } 163 | 164 | // Approximates to the closest valid color 165 | private static byte toNovationApprox(Color c) { 166 | List colors = Arrays.asList(NOVATION_COLORS); 167 | List colorDistance = colors.stream() 168 | .map(color -> { 169 | // redmean calc from https://www.compuphase.com/cmetric.htm 170 | int rbar = (c.getRed255() + color.getRed255()) / 2; 171 | return Math.sqrt( 172 | (2 + (rbar / 256.0)) * Math.pow(c.getRed255() - color.getRed255(), 2) + 173 | 4 * Math.pow(c.getGreen255() - color.getGreen255(), 2) + 174 | (2 + ((255 - rbar) / 256.0)) * Math.pow(c.getBlue255() - color.getBlue255(), 2) 175 | ); 176 | }) 177 | .collect(Collectors.toList()); 178 | return (byte) colorDistance.indexOf(Collections.min(colorDistance)); 179 | } 180 | 181 | // Use nicer approximations of Bitwig fixed colors 182 | public static byte toNovation(Color c) { 183 | return toNovationApprox(c); 184 | } 185 | 186 | public static String toHexString(byte... data) { 187 | StringBuilder builder = new StringBuilder(); 188 | for (byte dp : data) { 189 | builder.append(Character.forDigit((dp >> 4) & 0xF, 16)); 190 | builder.append(Character.forDigit(dp & 0xF, 16)); 191 | } 192 | return builder.toString(); 193 | } 194 | 195 | public static byte[] parseSysex(String sysex) { 196 | String message = sysex.substring(12, sysex.length() - 2); 197 | // System.out.println(sysex); 198 | byte[] bytes = new byte[message.length() / 2]; 199 | for (int i = 0; i < bytes.length; i++) { 200 | bytes[i] = (byte) (Integer.parseInt(message.substring(i * 2, i * 2 + 2), 16) & 0xff); 201 | } 202 | return bytes; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/internal/ChannelType.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.internal; 2 | 3 | public enum ChannelType { 4 | DAW, 5 | CUSTOM 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/internal/HostErrorOutputStream.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.internal; 2 | 3 | import com.bitwig.extension.controller.api.ControllerHost; 4 | 5 | import java.io.OutputStream; 6 | 7 | public class HostErrorOutputStream extends OutputStream { 8 | private final ControllerHost mHost; 9 | private String mBuffer = ""; 10 | 11 | 12 | public HostErrorOutputStream(ControllerHost host) { 13 | mHost = host; 14 | } 15 | 16 | @Override 17 | public void write(int b) { 18 | switch((char)b) { 19 | case '\n': 20 | mHost.errorln(mBuffer); 21 | mBuffer = ""; 22 | break; 23 | default: 24 | mBuffer += (char) b; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/internal/HostOutputStream.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.internal; 2 | 3 | import com.bitwig.extension.controller.api.ControllerHost; 4 | 5 | import java.io.OutputStream; 6 | 7 | /** 8 | * Buffers output for the ControllerHost, so we can use System.out.println... 9 | * @author Jengamon 10 | */ 11 | public class HostOutputStream extends OutputStream { 12 | private final ControllerHost mHost; 13 | private String mBuffer = ""; 14 | 15 | 16 | public HostOutputStream(ControllerHost host) { 17 | mHost = host; 18 | } 19 | 20 | @Override 21 | public void write(int b) { 22 | switch((char)b) { 23 | case '\n': 24 | mHost.println(mBuffer); 25 | mBuffer = ""; 26 | break; 27 | default: 28 | mBuffer += (char) b; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/internal/Session.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.internal; 2 | 3 | import com.bitwig.extension.callback.ShortMidiMessageReceivedCallback; 4 | import com.bitwig.extension.callback.SysexMidiDataReceivedCallback; 5 | import com.bitwig.extension.controller.api.ControllerHost; 6 | import com.bitwig.extension.controller.api.MidiIn; 7 | import com.bitwig.extension.controller.api.MidiOut; 8 | import com.bitwig.extension.controller.api.NoteInput; 9 | 10 | public class Session { 11 | private final MidiIn dawIn; 12 | private final MidiOut dawOut; 13 | 14 | private final MidiIn customIn; 15 | private final MidiOut customOut; 16 | 17 | private final NoteInput noteInput; 18 | 19 | private final static String SYSEX_HEADER = "f0 00 20 29 02 0c"; 20 | 21 | public Session(ControllerHost host) { 22 | dawIn = host.getMidiInPort(0); 23 | dawOut = host.getMidiOutPort(0); 24 | 25 | customIn = host.getMidiInPort(1); 26 | customOut = host.getMidiOutPort(1); 27 | 28 | noteInput = customIn.createNoteInput("", "??????"); 29 | noteInput.setShouldConsumeEvents(false); 30 | 31 | // Switch to Live mode (if not already) 32 | sendSysex("0e 00"); 33 | // Switch on DAW mode (if not already) 34 | sendSysex("10 01"); 35 | 36 | // forceSend(); 37 | } 38 | 39 | public void setMidiCallback(ChannelType type, ShortMidiMessageReceivedCallback clbk) { 40 | switch(type) { 41 | case DAW: 42 | dawIn.setMidiCallback(clbk); 43 | break; 44 | case CUSTOM: 45 | customIn.setMidiCallback(clbk); 46 | break; 47 | } 48 | } 49 | 50 | public void setSysexCallback(ChannelType type, SysexMidiDataReceivedCallback clbk) { 51 | switch(type) { 52 | case DAW: 53 | dawIn.setSysexCallback(clbk); 54 | break; 55 | case CUSTOM: 56 | customIn.setSysexCallback(clbk); 57 | break; 58 | } 59 | } 60 | 61 | public MidiIn midiIn(ChannelType type) { 62 | switch(type) { 63 | case DAW: 64 | return this.dawIn; 65 | case CUSTOM: 66 | return this.customIn; 67 | } 68 | return null; // Unreachable (hopefully) 69 | } 70 | 71 | public MidiOut midiOut(ChannelType type) { 72 | switch(type) { 73 | case DAW: 74 | return this.dawOut; 75 | case CUSTOM: 76 | return this.customOut; 77 | } 78 | return null; // Unreachable (hopefully) 79 | } 80 | 81 | public NoteInput noteInput() { 82 | return noteInput; 83 | } 84 | 85 | public void sendSysex(String message) { 86 | String sysex = SYSEX_HEADER + " " + message + " f7"; 87 | dawOut.sendSysex(sysex); 88 | } 89 | 90 | public void sendMidi(int status, int data1, int data2) { 91 | // if(status != 0) System.out.println(Utils.toHexString((byte)status) + "[" + Utils.toHexString((byte) data1) + " " + Utils.toHexString((byte) data2) + "]"); 92 | dawOut.sendMidi(status, data1, data2); 93 | } 94 | 95 | public void shutdown() { 96 | sendSysex("10 00"); 97 | // forceSend(); 98 | } 99 | 100 | @Deprecated 101 | public void forceSend() { 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/AbstractMode.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes; 2 | 3 | import com.bitwig.extension.controller.api.HardwareBinding; 4 | import io.github.jengamon.novation.Mode; 5 | import io.github.jengamon.novation.ModeMachine; 6 | import io.github.jengamon.novation.internal.Session; 7 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public abstract class AbstractMode { 13 | protected ModeMachine mModeMachine; 14 | private Mode mTarget; 15 | 16 | public final void onInit(ModeMachine machine, Mode target) { 17 | mModeMachine = machine; 18 | mTarget = target; 19 | } 20 | 21 | protected final void redraw(LaunchpadXSurface surface) { 22 | if(mModeMachine.mode() == mTarget) { 23 | mModeMachine.redraw(surface); 24 | } 25 | } 26 | 27 | public abstract List onBind(LaunchpadXSurface surface); 28 | public void onDraw(LaunchpadXSurface surface) {} 29 | public List processSysex(byte[] sysex) { return new ArrayList<>(); } 30 | public void finishedBind(Session session) {} 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/DrumPadMode.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.internal.Session; 6 | import io.github.jengamon.novation.surface.LaunchpadXPad; 7 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 8 | import io.github.jengamon.novation.surface.NoteButton; 9 | import io.github.jengamon.novation.surface.state.PadLightState; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | import java.util.concurrent.atomic.AtomicBoolean; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | public class DrumPadMode extends AbstractMode { 18 | private final AtomicInteger mChannel = new AtomicInteger(0); 19 | private final DrumPadLight[] drumPadLights = new DrumPadLight[64]; 20 | private final HardwareActionBindable[] mPlayNote; 21 | private final HardwareActionBindable[] mReleaseNote; 22 | private final AbsoluteHardwarControlBindable[] mAftertouchNote; 23 | private final AbsoluteHardwarControlBindable mChannelPressure; 24 | private final ArrowPadLight[] mArrowLights = new ArrowPadLight[4]; 25 | private final HardwareActionBindable[] mArrowActions = new HardwareActionBindable[4]; 26 | 27 | private class ArrowPadLight { 28 | private final int mOffset; 29 | private final IntegerValue mScrollPosition; 30 | private final ColorValue mTrackColor; 31 | public ArrowPadLight(LaunchpadXSurface surface, int offset, IntegerValue scrollPosition, ColorValue trackColor) { 32 | mOffset = offset; 33 | mScrollPosition = scrollPosition; 34 | mTrackColor = trackColor; 35 | 36 | mScrollPosition.addValueObserver(sp -> redraw(surface)); 37 | mTrackColor.addValueObserver((r, g, b) -> redraw(surface)); 38 | } 39 | 40 | public void draw(MultiStateHardwareLight arrowLight) { 41 | int testPos = mScrollPosition.get() + mOffset; 42 | if(testPos >= 0 && testPos < (128 - 63)) { 43 | arrowLight.setColor(mTrackColor.get()); 44 | } else { 45 | arrowLight.setColor(Color.nullColor()); 46 | } 47 | } 48 | } 49 | 50 | private class DrumPadLight { 51 | private final ColorValue mColor; 52 | private final AtomicBoolean mPlaying; 53 | private final BooleanValue mExists; 54 | private final BooleanValue mEnabled; 55 | public DrumPadLight(LaunchpadXSurface surface, DrumPad drumPad, AtomicBoolean playing) { 56 | mColor = drumPad.color(); 57 | mPlaying = playing; 58 | mExists = drumPad.exists(); 59 | mEnabled = drumPad.isActivated(); 60 | 61 | mColor.addValueObserver((r, g, b) -> redraw(surface)); 62 | mExists.addValueObserver(e -> redraw(surface)); 63 | mEnabled.addValueObserver(e -> redraw(surface)); 64 | } 65 | 66 | public void draw(MultiStateHardwareLight padLight) { 67 | if(mExists.get() && mEnabled.get()) { 68 | if(mPlaying.get()) { 69 | padLight.state().setValue(PadLightState.solidLight(78)); 70 | } else { 71 | padLight.setColor(mColor.get()); 72 | } 73 | } else { 74 | padLight.setColor(Color.nullColor()); 75 | } 76 | } 77 | } 78 | 79 | public DrumPadMode(ControllerHost host, Session session, LaunchpadXSurface surface, CursorDevice device) { 80 | BooleanValue mHasDrumPads = device.hasDrumPads(); 81 | mHasDrumPads.addValueObserver(hdp -> { 82 | if(hdp) { 83 | session.sendSysex("0f 01"); 84 | } else { 85 | session.sendSysex("0f 00"); 86 | } 87 | }); 88 | int[] arrowOffsets = new int[]{16, -16, -4, 4}; 89 | DrumPadBank mDrumBank = device.createDrumPadBank(64); 90 | SettableIntegerValue mScrollPosition = mDrumBank.scrollPosition(); 91 | AtomicInteger scrollPos = new AtomicInteger(0); 92 | mScrollPosition.addValueObserver(scrollPos::set); 93 | 94 | for(int i = 0; i < 4; i++) { 95 | int offset = arrowOffsets[i]; 96 | mArrowLights[i] = new ArrowPadLight(surface, offset, mScrollPosition, device.channel().color()); 97 | mArrowActions[i] = host.createAction(() -> { 98 | int newPos = scrollPos.get() + offset; 99 | if(newPos >= 0 && newPos < (128 - 63)) { 100 | mScrollPosition.inc(offset); 101 | } 102 | }, () -> "Scroll by " + offset); 103 | } 104 | 105 | mPlayNote = new HardwareActionBindable[64]; 106 | mReleaseNote = new HardwareActionBindable[64]; 107 | mAftertouchNote = new AbsoluteHardwarControlBindable[64]; 108 | 109 | NoteInput noteOut = session.noteInput(); 110 | 111 | for(int i = 0; i < 64; i++) { 112 | DrumPad dpad = mDrumBank.getItemAt(i); 113 | BooleanValue hasContent = dpad.exists(); 114 | hasContent.markInterested(); 115 | BooleanValue notDeactivated = dpad.isActivated(); 116 | notDeactivated.markInterested(); 117 | AtomicBoolean playing = new AtomicBoolean(false); 118 | 119 | drumPadLights[i] = new DrumPadLight(surface, dpad, playing); 120 | 121 | int finalI = i; 122 | dpad.playingNotes().addValueObserver((pns) -> { 123 | playing.set(Arrays.stream(pns).anyMatch((pn) -> pn.pitch() == finalI + mScrollPosition.get())); 124 | redraw(surface); 125 | }); 126 | mPlayNote[i] = host.createAction(val -> { 127 | if(hasContent.get() && notDeactivated.get()) { 128 | noteOut.sendRawMidiEvent(0x90 | (0xF & mChannel.get()), scrollPos.get() + finalI, (int)Math.round(val * 127)); 129 | playing.set(true); 130 | redraw(surface); 131 | } 132 | }, () -> "Play Drum Pad " + finalI); 133 | mReleaseNote[i] = host.createAction(() -> { 134 | noteOut.sendRawMidiEvent(0x80 | (0xF & mChannel.get()), scrollPos.get() + finalI, 0); 135 | playing.set(false); 136 | redraw(surface); 137 | }, () -> "Release Drum Pad " + finalI); 138 | mAftertouchNote[i] = host.createAbsoluteHardwareControlAdjustmentTarget(val -> { 139 | if(hasContent.get() && notDeactivated.get()) { 140 | noteOut.sendRawMidiEvent(0xA0 | (0xF & mChannel.get()), scrollPos.get() + finalI, (int)Math.round(val * 127)); 141 | } 142 | }); 143 | } 144 | 145 | mChannelPressure = host.createAbsoluteHardwareControlAdjustmentTarget(val -> 146 | noteOut.sendRawMidiEvent(0xD0 | (0xF & mChannel.get()), (int)Math.round(val * 127), 0) 147 | ); 148 | 149 | long channelQueryDelay = 32L; 150 | 151 | host.scheduleTask(new Runnable() { 152 | @Override 153 | public void run() { 154 | session.sendSysex("16"); 155 | host.scheduleTask(this, channelQueryDelay); 156 | } 157 | }, 1); 158 | } 159 | 160 | @Override 161 | public void onDraw(LaunchpadXSurface surface) { 162 | LaunchpadXPad[] arrows = surface.arrows(); 163 | for(int i = 0; i < arrows.length; i++) { 164 | mArrowLights[i].draw(arrows[i].light()); 165 | } 166 | 167 | for(NoteButton[] noteRow : surface.notes()) { 168 | for(NoteButton noteButton : noteRow) { 169 | int did = noteButton.drum_id() - 36; 170 | drumPadLights[did].draw(noteButton.light()); 171 | } 172 | } 173 | } 174 | 175 | @Override 176 | public List onBind(LaunchpadXSurface surface) { 177 | List bindings = new ArrayList<>(); 178 | for(NoteButton[] noteRow : surface.notes()) { 179 | for(NoteButton noteButton : noteRow) { 180 | int did = noteButton.drum_id() - 36; 181 | bindings.add(noteButton.button().pressedAction().addBinding(mPlayNote[did])); 182 | bindings.add(noteButton.button().releasedAction().addBinding(mReleaseNote[did])); 183 | bindings.add(noteButton.aftertouch().addBindingWithRange(mAftertouchNote[did], 0.0, 1.0)); 184 | } 185 | } 186 | 187 | LaunchpadXPad[] arrows = new LaunchpadXPad[] {surface.up(), surface.down(), surface.left(), surface.right()}; 188 | for(int i = 0; i < arrows.length; i++) { 189 | LaunchpadXPad pad = arrows[i]; 190 | bindings.add(pad.button().pressedAction().addBinding(mArrowActions[i])); 191 | } 192 | 193 | bindings.add(surface.channelPressure().addBindingWithRange(mChannelPressure, 0.0, 1.0)); 194 | 195 | return bindings; 196 | } 197 | 198 | @Override 199 | public List processSysex(byte[] sysex) { 200 | List responses = new ArrayList<>(); 201 | if (sysex[0] == 0x16) { 202 | mChannel.set(sysex[4]); 203 | } 204 | return responses; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/SessionMode.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.Utils; 6 | import io.github.jengamon.novation.internal.Session; 7 | import io.github.jengamon.novation.modes.session.ArrowPadLight; 8 | import io.github.jengamon.novation.modes.session.SessionPadLight; 9 | import io.github.jengamon.novation.surface.LaunchpadXPad; 10 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 11 | import io.github.jengamon.novation.surface.state.PadLightState; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.concurrent.atomic.AtomicBoolean; 16 | 17 | public class SessionMode extends AbstractMode { 18 | private final SessionSceneLight[] sceneLights = new SessionSceneLight[8]; 19 | private final HardwareActionBindable[] sceneLaunchActions = new HardwareActionBindable[8]; 20 | private final HardwareActionBindable[] sceneLaunchReleaseActions = new HardwareActionBindable[8]; 21 | private final SessionPadLight[][] padLights = new SessionPadLight[8][8]; 22 | private final HardwareActionBindable[][] padActions = new HardwareActionBindable[8][8]; 23 | private final HardwareActionBindable[][] padReleaseActions = new HardwareActionBindable[8][8]; 24 | private final ArrowPadLight[] arrowLights = new ArrowPadLight[4]; 25 | private final HardwareBindable[] arrowActions; 26 | 27 | private class SessionSceneLight { 28 | private final RangedValue mBPM; 29 | private final BooleanValue mPulseSessionPads; 30 | private final ColorValue mSceneColor; 31 | private final BooleanValue mSceneExists; 32 | 33 | public SessionSceneLight(LaunchpadXSurface surface, Scene scene, BooleanValue pulseSessionPads, RangedValue bpm) { 34 | mBPM = bpm; 35 | mPulseSessionPads = pulseSessionPads; 36 | mSceneColor = scene.color(); 37 | mSceneExists = scene.exists(); 38 | 39 | mSceneColor.addValueObserver((r, g, b) -> redraw(surface)); 40 | mSceneExists.addValueObserver(e -> redraw(surface)); 41 | mBPM.addValueObserver(b -> redraw(surface)); 42 | mPulseSessionPads.addValueObserver(p -> redraw(surface)); 43 | } 44 | 45 | public void draw(MultiStateHardwareLight sceneLight) { 46 | Color baseColor = mSceneColor.get(); 47 | if (mSceneExists.get()) { 48 | if (mPulseSessionPads.get()) { 49 | sceneLight.state().setValue(PadLightState.pulseLight(mBPM.getRaw(), Utils.toNovation(baseColor))); 50 | } else { 51 | sceneLight.setColor(baseColor); 52 | } 53 | } 54 | } 55 | } 56 | 57 | public SessionMode(TrackBank bank, Transport transport, LaunchpadXSurface surface, ControllerHost host, BooleanValue pulseSessionPads, AtomicBoolean launchAlt) { 58 | // int[] ids = new int[]{89, 79, 69, 59, 49, 39, 29, 19}; 59 | RangedValue bpm = transport.tempo().modulatedValue(); 60 | 61 | // Set up scene buttons 62 | for (int i = 0; i < 8; i++) { 63 | Scene scene = bank.sceneBank().getItemAt(i); 64 | sceneLights[i] = new SessionSceneLight(surface, scene, pulseSessionPads, bpm); 65 | int finalI = i; 66 | sceneLaunchActions[i] = host.createAction(() -> { 67 | if (launchAlt.get()) { 68 | scene.launchAlt(); 69 | } else { 70 | scene.launch(); 71 | } 72 | scene.selectInEditor(); 73 | }, () -> "Press Scene " + finalI); 74 | sceneLaunchReleaseActions[i] = host.createAction(() -> { 75 | if (launchAlt.get()) { 76 | scene.launchReleaseAlt(); 77 | } else { 78 | scene.launchRelease(); 79 | } 80 | }, () -> "Release Scene " + finalI); 81 | } 82 | 83 | // Setup pad lights and buttons 84 | /* 85 | The indicies map the pad out as 86 | 0,0.......0,7 87 | 1,0.......1,7 88 | . . 89 | . . 90 | . . 91 | 7,0.......7,7 92 | 93 | since we want scenes to go down, we simply mark the indicies as (scene, track) 94 | */ 95 | for (int scene = 0; scene < 8; scene++) { 96 | padActions[scene] = new HardwareActionBindable[8]; 97 | padLights[scene] = new SessionPadLight[8]; 98 | for (int trk = 0; trk < 8; trk++) { 99 | Track track = bank.getItemAt(trk); 100 | ClipLauncherSlotBank slotBank = track.clipLauncherSlotBank(); 101 | ClipLauncherSlot slot = slotBank.getItemAt(scene); 102 | 103 | int finalTrk = trk; 104 | int finalScene = scene; 105 | padLights[scene][trk] = new SessionPadLight(surface, slot, track, bpm, this::redraw, scene); 106 | padActions[scene][trk] = host.createAction(() -> { 107 | if (launchAlt.get()) { 108 | slot.launchAlt(); 109 | } else { 110 | slot.launch(); 111 | } 112 | }, () -> "Press Scene " + finalScene + " Track " + finalTrk); 113 | padReleaseActions[scene][trk] = host.createAction(() -> { 114 | if (launchAlt.get()) { 115 | slot.launchReleaseAlt(); 116 | } else { 117 | slot.launchRelease(); 118 | } 119 | }, () -> "Release Scene " + finalScene + " Track " + finalTrk); 120 | } 121 | } 122 | 123 | arrowActions = new HardwareActionBindable[]{ 124 | bank.sceneBank().scrollBackwardsAction(), 125 | bank.sceneBank().scrollForwardsAction(), 126 | bank.scrollBackwardsAction(), 127 | bank.scrollForwardsAction() 128 | }; 129 | 130 | BooleanValue[] arrowEnabled = new BooleanValue[]{ 131 | bank.sceneBank().canScrollBackwards(), 132 | bank.sceneBank().canScrollForwards(), 133 | bank.canScrollBackwards(), 134 | bank.canScrollForwards() 135 | }; 136 | 137 | LaunchpadXPad[] arrows = surface.arrows(); 138 | for (int i = 0; i < arrows.length; i++) { 139 | arrowLights[i] = new ArrowPadLight(surface, arrowEnabled[i], this::redraw); 140 | } 141 | } 142 | 143 | @Override 144 | public List onBind(LaunchpadXSurface surface) { 145 | List bindings = new ArrayList<>(); 146 | for (int i = 0; i < 8; i++) { 147 | bindings.add(surface.scenes()[i].button().pressedAction().addBinding(sceneLaunchActions[i])); 148 | bindings.add(surface.scenes()[i].button().releasedAction().addBinding(sceneLaunchReleaseActions[i])); 149 | } 150 | for (int i = 0; i < 8; i++) { 151 | for (int j = 0; j < 8; j++) { 152 | bindings.add(surface.notes()[i][j].button().pressedAction().addBinding(padActions[i][j])); 153 | bindings.add(surface.notes()[i][j].button().releasedAction().addBinding(padReleaseActions[i][j])); 154 | } 155 | } 156 | LaunchpadXPad[] arrows = surface.arrows(); 157 | for (int i = 0; i < 4; i++) { 158 | bindings.add(arrows[i].button().pressedAction().addBinding(arrowActions[i])); 159 | } 160 | return bindings; 161 | } 162 | 163 | @Override 164 | public void onDraw(LaunchpadXSurface surface) { 165 | LaunchpadXPad[] scenes = surface.scenes(); 166 | for (int i = 0; i < scenes.length; i++) { 167 | sceneLights[i].draw(scenes[i].light()); 168 | } 169 | LaunchpadXPad[] arrows = surface.arrows(); 170 | for (int i = 0; i < arrows.length; i++) { 171 | arrowLights[i].draw(surface.arrows()[i].light()); 172 | } 173 | LaunchpadXPad[][] pads = surface.notes(); 174 | for (int i = 0; i < pads.length; i++) { 175 | for (int j = 0; j < pads[i].length; j++) { 176 | padLights[i][j].draw(pads[i][j].light()); 177 | } 178 | } 179 | } 180 | 181 | @Override 182 | public void finishedBind(Session session) { 183 | session.sendSysex("14 00 00"); 184 | session.sendSysex("00 00"); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/AbstractFaderMixerMode.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.controller.api.ControllerHost; 4 | import com.bitwig.extension.controller.api.Transport; 5 | import io.github.jengamon.novation.Mode; 6 | import io.github.jengamon.novation.internal.Session; 7 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 8 | 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | /** 12 | * Switches the Launchpad to fader mode, when all bindings are complete 13 | */ 14 | public abstract class AbstractFaderMixerMode extends AbstractMixerMode { 15 | 16 | public AbstractFaderMixerMode(AtomicReference mixerMode, ControllerHost host, 17 | Transport transport, LaunchpadXSurface lSurf, Mode targetMode, int modeColor) { 18 | super(mixerMode, host, transport, lSurf, targetMode, modeColor); 19 | } 20 | 21 | @Override 22 | public void finishedBind(Session session) { 23 | super.finishedBind(session); 24 | session.sendSysex("00 0D"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/AbstractMixerMode.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Mode; 5 | import io.github.jengamon.novation.internal.Session; 6 | import io.github.jengamon.novation.modes.AbstractMode; 7 | import io.github.jengamon.novation.surface.LaunchpadXPad; 8 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 9 | import io.github.jengamon.novation.surface.state.PadLightState; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.concurrent.atomic.AtomicReference; 14 | 15 | public abstract class AbstractMixerMode extends AbstractMode { 16 | protected RangedValue mBPM; 17 | protected AtomicReference mMixerMode; 18 | private final Mode mTargetMode; 19 | 20 | private static final Mode[] scenemodes = new Mode[] { 21 | Mode.MIXER_VOLUME, 22 | Mode.MIXER_PAN, 23 | Mode.MIXER_SEND, 24 | Mode.MIXER_CONTROLS, 25 | Mode.MIXER_STOP, 26 | Mode.MIXER_MUTE, 27 | Mode.MIXER_SOLO, 28 | Mode.MIXER_ARM 29 | }; 30 | private final HardwareActionBindable[] sceneActions = new HardwareActionBindable[8]; 31 | protected int mModeColor; 32 | 33 | public AbstractMixerMode(AtomicReference mixerMode, ControllerHost host, 34 | Transport transport, LaunchpadXSurface lSurf, Mode targetMode, int modeColor) { 35 | mBPM = transport.tempo().modulatedValue(); 36 | mMixerMode = mixerMode; 37 | mTargetMode = targetMode; 38 | 39 | mBPM.markInterested(); 40 | mModeColor = modeColor; 41 | 42 | for(int i = 0; i < 8; i++) { 43 | final int j = i; 44 | sceneActions[i] = host.createAction(() -> mModeMachine.setMode(lSurf, scenemodes[j]), () -> "Set mode to " + scenemodes[j]); 45 | } 46 | } 47 | 48 | public void drawMixerModeIndicator(LaunchpadXSurface surface, int padIndex) { 49 | for(int i = 0; i < 8; i++) { 50 | LaunchpadXPad scene = surface.scenes()[i]; 51 | if(i == padIndex) { 52 | scene.light().state().setValue(PadLightState.pulseLight(mBPM.getRaw(), mModeColor)); 53 | } else { 54 | scene.light().state().setValue(PadLightState.solidLight(1)); 55 | } 56 | } 57 | } 58 | 59 | @Override 60 | public List onBind(LaunchpadXSurface surface) { 61 | List bindings = new ArrayList<>(); 62 | 63 | for(int i = 0; i < 8; i++) { 64 | LaunchpadXPad scene = surface.scenes()[i]; 65 | bindings.add(scene.button().pressedAction().addBinding(sceneActions[i])); 66 | } 67 | 68 | return bindings; 69 | } 70 | 71 | @Override 72 | public void finishedBind(Session session) { 73 | session.sendSysex("14 6C 02"); 74 | mMixerMode.set(mTargetMode); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/AbstractSessionMixerMode.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Mode; 5 | import io.github.jengamon.novation.internal.Session; 6 | import io.github.jengamon.novation.modes.session.ArrowPadLight; 7 | import io.github.jengamon.novation.modes.session.SessionPadLight; 8 | import io.github.jengamon.novation.surface.LaunchpadXPad; 9 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 10 | import io.github.jengamon.novation.surface.NoteButton; 11 | 12 | import java.util.List; 13 | import java.util.concurrent.atomic.AtomicBoolean; 14 | import java.util.concurrent.atomic.AtomicReference; 15 | 16 | public abstract class AbstractSessionMixerMode extends AbstractMixerMode { 17 | private final SessionPadLight[][] padLights = new SessionPadLight[7][8]; 18 | private final HardwareActionBindable[][] padActions = new HardwareActionBindable[7][8]; 19 | private final HardwareActionBindable[][] padReleaseActions = new HardwareActionBindable[7][8]; 20 | private final ArrowPadLight[] arrowLights = new ArrowPadLight[4]; 21 | private final HardwareBindable[] arrowActions; 22 | 23 | public AbstractSessionMixerMode(AtomicReference mixerMode, ControllerHost host, 24 | Transport transport, LaunchpadXSurface surface, TrackBank bank, Mode targetMode, int modeColor, AtomicBoolean launchAlt) { 25 | super(mixerMode, host, transport, surface, targetMode, modeColor); 26 | 27 | // Setup pad lights and buttons 28 | /* 29 | The indicies map the pad out as 30 | 0,0.......0,7 31 | 1,0.......1,7 32 | . . 33 | . . 34 | . . 35 | 7,0.......7,7 36 | 37 | since we want scenes to go down, we simply mark the indicies as (scene, track) 38 | */ 39 | for(int scene = 0; scene < 7; scene++) { 40 | padActions[scene] = new HardwareActionBindable[8]; 41 | padLights[scene] = new SessionPadLight[8]; 42 | 43 | for(int trk = 0; trk < 8; trk++) { 44 | Track track = bank.getItemAt(trk); 45 | ClipLauncherSlotBank slotBank = track.clipLauncherSlotBank(); 46 | ClipLauncherSlot slot = slotBank.getItemAt(scene); 47 | 48 | final int finalScene = scene; 49 | final int finalTrk = trk; 50 | padLights[scene][trk] = new SessionPadLight(surface, slot, track, mBPM, this::redraw, scene); 51 | padActions[scene][trk] = host.createAction(() -> { 52 | if (launchAlt.get()) { 53 | slot.launchAlt(); 54 | } else { 55 | slot.launch(); 56 | } 57 | }, () -> "Press Scene " + finalScene + " Track " + finalTrk); 58 | padReleaseActions[scene][trk] = host.createAction(() -> { 59 | if (launchAlt.get()) { 60 | slot.launchReleaseAlt(); 61 | } else { 62 | slot.launchRelease(); 63 | } 64 | }, () -> "Release Scene " + finalScene + " Track " + finalTrk); 65 | } 66 | } 67 | 68 | arrowActions = new HardwareActionBindable[] { 69 | bank.sceneBank().scrollBackwardsAction(), 70 | bank.sceneBank().scrollForwardsAction(), 71 | bank.scrollBackwardsAction(), 72 | bank.scrollForwardsAction() 73 | }; 74 | 75 | BooleanValue[] arrowEnabled = new BooleanValue[]{ 76 | bank.sceneBank().canScrollBackwards(), 77 | bank.sceneBank().canScrollForwards(), 78 | bank.canScrollBackwards(), 79 | bank.canScrollForwards() 80 | }; 81 | 82 | LaunchpadXPad[] arrows = surface.arrows(); 83 | for(int i = 0; i < arrows.length; i++) { 84 | arrowLights[i] = new ArrowPadLight(surface, arrowEnabled[i], mModeColor, this::redraw); 85 | } 86 | } 87 | 88 | protected final NoteButton[] getFinalRow(LaunchpadXSurface surface) { 89 | return surface.notes()[7]; 90 | } 91 | 92 | @Override 93 | public void onDraw(LaunchpadXSurface surface) { 94 | super.onDraw(surface); 95 | 96 | LaunchpadXPad[] arrows = surface.arrows(); 97 | for(int i = 0; i < arrows.length; i++) { 98 | arrowLights[i].draw(arrows[i].light()); 99 | } 100 | 101 | LaunchpadXPad[][] pads = surface.notes(); 102 | for(int i = 0; i < 7; i++) { 103 | for(int j = 0; j < 8; j++) { 104 | padLights[i][j].draw(pads[i][j].light()); 105 | } 106 | } 107 | } 108 | 109 | @Override 110 | public List onBind(LaunchpadXSurface surface) { 111 | List bindings = super.onBind(surface); 112 | 113 | for(int i = 0; i < 7; i++) { 114 | for(int j = 0; j < 8; j++) { 115 | NoteButton button = surface.notes()[i][j]; 116 | bindings.add(button.button().pressedAction().addBinding(padActions[i][j])); 117 | bindings.add(button.button().releasedAction().addBinding(padReleaseActions[i][j])); 118 | } 119 | } 120 | LaunchpadXPad[] arrows = new LaunchpadXPad[]{surface.up(), surface.down(), surface.left(), surface.right()}; 121 | for(int i = 0; i < 4; i++) { 122 | bindings.add(arrows[i].button().pressedAction().addBinding(arrowActions[i])); 123 | } 124 | 125 | return bindings; 126 | } 127 | 128 | @Override 129 | public void finishedBind(Session session) { 130 | super.finishedBind(session); 131 | session.sendSysex("00 00"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/ControlsMixer.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.Mode; 6 | import io.github.jengamon.novation.surface.Fader; 7 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 8 | import io.github.jengamon.novation.surface.state.FaderLightState; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicReference; 12 | 13 | public class ControlsMixer extends AbstractFaderMixerMode { 14 | private final FixedFaderLight[] faderLights = new FixedFaderLight[8]; 15 | private final Parameter[] controls = new Parameter[8]; 16 | private static final byte[] CONTROL_TAGS = new byte[] { 17 | (byte)0x05, 18 | (byte)0x54, 19 | (byte)0x0d, 20 | (byte)0x15, 21 | (byte)0x1d, 22 | (byte)0x25, 23 | (byte)0x35, 24 | (byte)0x39, 25 | }; 26 | 27 | private class FixedFaderLight { 28 | private final byte mColor; 29 | private final BooleanValue mExists; 30 | public FixedFaderLight(LaunchpadXSurface surface, byte color, BooleanValue exists) { 31 | mExists = exists; 32 | mColor = color; 33 | 34 | mExists.addValueObserver(e -> redraw(surface)); 35 | } 36 | 37 | public void draw(MultiStateHardwareLight light) { 38 | if(mExists.get()) { 39 | light.state().setValue(new FaderLightState(mColor)); 40 | } else { 41 | light.setColor(Color.nullColor()); 42 | } 43 | } 44 | } 45 | 46 | public ControlsMixer(AtomicReference mixerMode, ControllerHost host, Transport transport, 47 | LaunchpadXSurface surface, CursorDevice device) { 48 | super(mixerMode, host, transport, surface, Mode.MIXER_CONTROLS, 68); 49 | 50 | CursorRemoteControlsPage controlPage = device.createCursorRemoteControlsPage(8); 51 | 52 | for(int i = 0; i < 8; i++) { 53 | RemoteControl control = controlPage.getParameter(i); 54 | 55 | byte faderColor = CONTROL_TAGS[i]; 56 | faderLights[i] = new FixedFaderLight(surface, faderColor, control.exists()); 57 | controls[i] = control; 58 | } 59 | } 60 | 61 | @Override 62 | public void onDraw(LaunchpadXSurface surface) { 63 | super.onDraw(surface); 64 | 65 | drawMixerModeIndicator(surface, 3); 66 | 67 | Fader[] faders = surface.faders(); 68 | for(int i = 0; i < 8; i++) { 69 | faderLights[i].draw(faders[i].light()); 70 | } 71 | } 72 | 73 | @Override 74 | public List onBind(LaunchpadXSurface surface) { 75 | List list = super.onBind(surface); 76 | 77 | // Figure out if the faders should be bipolar or not 78 | // WARNING Very hacky 79 | boolean[] bipolar = new boolean[8]; 80 | for(int i = 0; i < bipolar.length; i++) { 81 | // TODO Determine if parameter is bipolar or not 82 | bipolar[i] = false; 83 | } 84 | 85 | // Enable faders 86 | surface.setupFaders(true, bipolar, 45); 87 | 88 | for(int i = 0; i < 8; i++) { 89 | Fader controlFader = surface.faders()[i]; 90 | list.add(controls[i].addBinding(controlFader.fader())); 91 | } 92 | 93 | return list; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/MuteMixer.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.Mode; 6 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 7 | import io.github.jengamon.novation.surface.NoteButton; 8 | import io.github.jengamon.novation.surface.state.PadLightState; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | public class MuteMixer extends AbstractSessionMixerMode { 15 | private final MuteRowPadLight[] mMutePads = new MuteRowPadLight[8]; 16 | private final HardwareActionBindable[] mMuteAction = new HardwareActionBindable[8]; 17 | 18 | private class MuteRowPadLight { 19 | private final BooleanValue mMute; 20 | private final BooleanValue mExists; 21 | public MuteRowPadLight(LaunchpadXSurface surface, Track track) { 22 | mMute = track.mute(); 23 | mExists = track.exists(); 24 | 25 | mMute.addValueObserver(s -> redraw(surface)); 26 | mExists.addValueObserver(e -> redraw(surface)); 27 | } 28 | 29 | public void draw(MultiStateHardwareLight light) { 30 | if(mExists.get()) { 31 | if(mMute.get()) { 32 | light.state().setValue(PadLightState.solidLight(9)); 33 | } else { 34 | light.state().setValue(PadLightState.solidLight(11)); 35 | } 36 | } else { 37 | light.setColor(Color.nullColor()); 38 | } 39 | } 40 | } 41 | 42 | public MuteMixer(AtomicReference mixerMode, ControllerHost host, Transport transport, 43 | LaunchpadXSurface surface, TrackBank bank, AtomicBoolean launchAlt) { 44 | super(mixerMode, host, transport, surface, bank, Mode.MIXER_MUTE, 9, launchAlt); 45 | 46 | for(int i = 0; i < 8; i++) { 47 | Track track = bank.getItemAt(i); 48 | mMutePads[i] = new MuteRowPadLight(surface, track); 49 | mMuteAction[i] = track.mute().toggleAction(); 50 | } 51 | } 52 | 53 | @Override 54 | public void onDraw(LaunchpadXSurface surface) { 55 | super.onDraw(surface); 56 | 57 | drawMixerModeIndicator(surface, 5); 58 | 59 | NoteButton[] finalRow = getFinalRow(surface); 60 | for(int i = 0; i < finalRow.length; i++) { 61 | mMutePads[i].draw(finalRow[i].light()); 62 | } 63 | } 64 | 65 | @Override 66 | public List onBind(LaunchpadXSurface surface) { 67 | List list = super.onBind(surface); 68 | 69 | NoteButton[] finalRow = getFinalRow(surface); 70 | for(int i = 0; i < finalRow.length; i++) { 71 | NoteButton pad = finalRow[i]; 72 | list.add(pad.button().pressedAction().addBinding(mMuteAction[i])); 73 | } 74 | 75 | return list; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/PanMixer.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Mode; 5 | import io.github.jengamon.novation.modes.session.ArrowPadLight; 6 | import io.github.jengamon.novation.modes.session.TrackColorFaderLight; 7 | import io.github.jengamon.novation.surface.Fader; 8 | import io.github.jengamon.novation.surface.LaunchpadXPad; 9 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | public class PanMixer extends AbstractFaderMixerMode { 15 | private final TrackColorFaderLight[] faderLights = new TrackColorFaderLight[8]; 16 | private final Parameter[] pans = new Parameter[8]; 17 | 18 | private final ArrowPadLight trackForwardLight; 19 | private final ArrowPadLight trackBackwardLight; 20 | private final HardwareActionBindable trackForwardAction; 21 | private final HardwareActionBindable trackBackwardAction; 22 | 23 | public PanMixer(AtomicReference mixerMode, ControllerHost host, Transport transport, 24 | LaunchpadXSurface surface, TrackBank bank) { 25 | super(mixerMode, host, transport, surface, Mode.MIXER_VOLUME, 80); 26 | 27 | for(int i = 0; i < 8; i++) { 28 | Track track = bank.getItemAt(i); 29 | faderLights[i] = new TrackColorFaderLight(surface, track, this::redraw); 30 | pans[i] = track.pan(); 31 | } 32 | 33 | trackForwardLight = new ArrowPadLight(surface, bank.canScrollForwards(), mModeColor, this::redraw); 34 | trackBackwardLight = new ArrowPadLight(surface, bank.canScrollBackwards(), mModeColor, this::redraw); 35 | trackForwardAction = bank.scrollForwardsAction(); 36 | trackBackwardAction = bank.scrollBackwardsAction(); 37 | } 38 | 39 | private LaunchpadXPad getBack(LaunchpadXSurface surface) { return surface.up(); } 40 | private LaunchpadXPad getForward(LaunchpadXSurface surface) { return surface.down(); } 41 | 42 | @Override 43 | public void onDraw(LaunchpadXSurface surface) { 44 | super.onDraw(surface); 45 | 46 | drawMixerModeIndicator(surface, 1); 47 | 48 | Fader[] faders = surface.faders(); 49 | for(int i = 0; i < faders.length; i++) { 50 | faderLights[i].draw(faders[i].light()); 51 | } 52 | 53 | LaunchpadXPad back = getBack(surface); 54 | LaunchpadXPad frwd = getForward(surface); 55 | trackBackwardLight.draw(back.light()); 56 | trackForwardLight.draw(frwd.light()); 57 | } 58 | 59 | @Override 60 | public List onBind(LaunchpadXSurface surface) { 61 | List list = super.onBind(surface); 62 | 63 | // Enable faders (and bind to proper set) 64 | surface.setupFaders(false, true, 29); 65 | 66 | LaunchpadXPad back = getBack(surface); 67 | LaunchpadXPad frwd = getForward(surface); 68 | list.add(back.button().pressedAction().addBinding(trackBackwardAction)); 69 | list.add(frwd.button().pressedAction().addBinding(trackForwardAction)); 70 | 71 | // Bind faders 72 | for(int i = 0; i < 8; i++) { 73 | Fader panFader = surface.faders()[i]; 74 | list.add(pans[i].addBinding(panFader.fader())); 75 | } 76 | 77 | return list; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/RecordArmMixer.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.Mode; 6 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 7 | import io.github.jengamon.novation.surface.NoteButton; 8 | import io.github.jengamon.novation.surface.state.PadLightState; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | public class RecordArmMixer extends AbstractSessionMixerMode { 15 | private final ArmRowPadLight[] mArmPads = new ArmRowPadLight[8]; 16 | private final HardwareActionBindable[] mArmAction = new HardwareActionBindable[8]; 17 | 18 | private class ArmRowPadLight { 19 | private final BooleanValue mArm; 20 | private final BooleanValue mHasNoteInput; 21 | private final BooleanValue mHasAudioInput; 22 | private final BooleanValue mExists; 23 | public ArmRowPadLight(LaunchpadXSurface surface, Track track) { 24 | mArm = track.arm(); 25 | mExists = track.exists(); 26 | mHasAudioInput = track.sourceSelector().hasAudioInputSelected(); 27 | mHasNoteInput = track.sourceSelector().hasNoteInputSelected(); 28 | 29 | mArm.addValueObserver(s -> redraw(surface)); 30 | mExists.addValueObserver(e -> redraw(surface)); 31 | mHasNoteInput.addValueObserver(n -> redraw(surface)); 32 | mHasAudioInput.addValueObserver(a -> redraw(surface)); 33 | } 34 | 35 | public void draw(MultiStateHardwareLight light) { 36 | if(mExists.get()) { 37 | if(mArm.get()) { 38 | light.state().setValue(PadLightState.solidLight(120)); 39 | } else if (mHasNoteInput.get() || mHasAudioInput.get()) { 40 | light.state().setValue(PadLightState.solidLight(121)); 41 | } else { 42 | light.setColor(Color.nullColor()); 43 | } 44 | } else { 45 | light.setColor(Color.nullColor()); 46 | } 47 | } 48 | } 49 | 50 | public RecordArmMixer(AtomicReference mixerMode, ControllerHost host, Transport transport, 51 | LaunchpadXSurface surface, TrackBank bank, AtomicBoolean launchAlt) { 52 | super(mixerMode, host, transport, surface, bank, Mode.MIXER_ARM, 120, launchAlt); 53 | 54 | for(int i = 0; i < 8; i++) { 55 | Track track = bank.getItemAt(i); 56 | 57 | mArmPads[i] = new ArmRowPadLight(surface, track); 58 | mArmAction[i] = track.arm().toggleAction(); 59 | } 60 | } 61 | 62 | @Override 63 | public void onDraw(LaunchpadXSurface surface) { 64 | super.onDraw(surface); 65 | 66 | drawMixerModeIndicator(surface, 7); 67 | 68 | NoteButton[] finalRow = getFinalRow(surface); 69 | for(int i = 0; i < finalRow.length; i++) { 70 | mArmPads[i].draw(finalRow[i].light()); 71 | } 72 | } 73 | 74 | @Override 75 | public List onBind(LaunchpadXSurface surface) { 76 | List list = super.onBind(surface); 77 | 78 | // Bind the final row of pads 79 | NoteButton[] finalRow = getFinalRow(surface); 80 | for(int i = 0; i < finalRow.length; i++) { 81 | NoteButton pad = finalRow[i]; 82 | list.add(pad.button().pressedAction().setBinding(mArmAction[i])); 83 | } 84 | 85 | return list; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/SendMixer.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Mode; 5 | import io.github.jengamon.novation.modes.session.ArrowPadLight; 6 | import io.github.jengamon.novation.modes.session.TrackColorFaderLight; 7 | import io.github.jengamon.novation.surface.Fader; 8 | import io.github.jengamon.novation.surface.LaunchpadXPad; 9 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | public class SendMixer extends AbstractFaderMixerMode { 15 | private final TrackColorFaderLight[] faderLights = new TrackColorFaderLight[8]; 16 | private final Parameter[] sends = new Parameter[8]; 17 | 18 | private final ArrowPadLight trackForwardLight; 19 | private final ArrowPadLight trackBackwardLight; 20 | private final HardwareActionBindable trackForwardAction; 21 | private final HardwareActionBindable trackBackwardAction; 22 | 23 | public SendMixer(AtomicReference mixerMode, ControllerHost host, Transport transport, 24 | LaunchpadXSurface surface, CursorTrack track) { 25 | super(mixerMode, host, transport, surface, Mode.MIXER_SEND, 82); 26 | 27 | SendBank bank = track.sendBank(); 28 | 29 | for(int i = 0; i < 8; i++) { 30 | Send send = bank.getItemAt(i); 31 | faderLights[i] = new TrackColorFaderLight(surface, send, this::redraw); 32 | sends[i] = send; 33 | } 34 | 35 | trackForwardLight = new ArrowPadLight(surface, bank.canScrollForwards(), mModeColor, this::redraw); 36 | trackBackwardLight = new ArrowPadLight(surface, bank.canScrollBackwards(), mModeColor, this::redraw); 37 | trackForwardAction = bank.scrollForwardsAction(); 38 | trackBackwardAction = bank.scrollBackwardsAction(); 39 | } 40 | 41 | private LaunchpadXPad getBack(LaunchpadXSurface surface) { return surface.left(); } 42 | private LaunchpadXPad getForward(LaunchpadXSurface surface) { return surface.right(); } 43 | 44 | @Override 45 | public void onDraw(LaunchpadXSurface surface) { 46 | super.onDraw(surface); 47 | 48 | drawMixerModeIndicator(surface, 2); 49 | 50 | Fader[] faders = surface.faders(); 51 | for(int i = 0; i < faders.length; i++) { 52 | faderLights[i].draw(faders[i].light()); 53 | } 54 | 55 | LaunchpadXPad back = getBack(surface); 56 | LaunchpadXPad frwd = getForward(surface); 57 | trackBackwardLight.draw(back.light()); 58 | trackForwardLight.draw(frwd.light()); 59 | } 60 | 61 | @Override 62 | public List onBind(LaunchpadXSurface surface) { 63 | List list = super.onBind(surface); 64 | 65 | // Enable faders 66 | surface.setupFaders(true, false, 37); 67 | 68 | LaunchpadXPad back = getBack(surface); 69 | LaunchpadXPad frwd = getForward(surface); 70 | list.add(back.button().pressedAction().addBinding(trackBackwardAction)); 71 | list.add(frwd.button().pressedAction().addBinding(trackForwardAction)); 72 | 73 | for(int i = 0; i < 8; i++) { 74 | Fader sendFader = surface.faders()[i]; 75 | list.add(sends[i].addBinding(sendFader.fader())); 76 | } 77 | 78 | return list; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/SoloMixer.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.Mode; 6 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 7 | import io.github.jengamon.novation.surface.NoteButton; 8 | import io.github.jengamon.novation.surface.state.PadLightState; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | public class SoloMixer extends AbstractSessionMixerMode { 15 | private final SoloRowPadLight[] mSoloPads = new SoloRowPadLight[8]; 16 | private final HardwareActionBindable[] mSoloAction = new HardwareActionBindable[8]; 17 | 18 | private class SoloRowPadLight { 19 | private final BooleanValue mSolo; 20 | private final BooleanValue mExists; 21 | public SoloRowPadLight(LaunchpadXSurface surface, Track track) { 22 | mSolo = track.solo(); 23 | mExists = track.exists(); 24 | 25 | mSolo.addValueObserver(s -> redraw(surface)); 26 | mExists.addValueObserver(e -> redraw(surface)); 27 | } 28 | 29 | public void draw(MultiStateHardwareLight light) { 30 | if(mExists.get()) { 31 | if(mSolo.get()) { 32 | light.state().setValue(PadLightState.solidLight(124)); 33 | } else { 34 | light.state().setValue(PadLightState.solidLight(125)); 35 | } 36 | } else { 37 | light.setColor(Color.nullColor()); 38 | } 39 | } 40 | } 41 | 42 | public SoloMixer(AtomicReference mixerMode, ControllerHost host, Transport transport, 43 | LaunchpadXSurface surface, TrackBank bank, AtomicBoolean launchAlt) { 44 | super(mixerMode, host, transport, surface, bank, Mode.MIXER_SOLO, 124, launchAlt); 45 | 46 | for(int i = 0; i < 8; i++) { 47 | Track track = bank.getItemAt(i); 48 | mSoloPads[i] = new SoloRowPadLight(surface, track); 49 | mSoloAction[i] = track.solo().toggleAction(); 50 | } 51 | } 52 | 53 | @Override 54 | public void onDraw(LaunchpadXSurface surface) { 55 | super.onDraw(surface); 56 | 57 | drawMixerModeIndicator(surface, 6); 58 | 59 | NoteButton[] finalRow = getFinalRow(surface); 60 | for(int i = 0; i < finalRow.length; i++) { 61 | mSoloPads[i].draw(finalRow[i].light()); 62 | } 63 | } 64 | 65 | @Override 66 | public List onBind(LaunchpadXSurface surface) { 67 | List list = super.onBind(surface); 68 | 69 | NoteButton[] finalRow = getFinalRow(surface); 70 | for(int i = 0; i < finalRow.length; i++) { 71 | NoteButton pad = finalRow[i]; 72 | list.add(pad.button().pressedAction().addBinding(mSoloAction[i])); 73 | } 74 | 75 | return list; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/StopClipMixer.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.Mode; 6 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 7 | import io.github.jengamon.novation.surface.NoteButton; 8 | import io.github.jengamon.novation.surface.state.PadLightState; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | public class StopClipMixer extends AbstractSessionMixerMode { 15 | private final StopRowPadLight[] mStopPads = new StopRowPadLight[8]; 16 | private final HardwareActionBindable[] mStopAction = new HardwareActionBindable[8]; 17 | 18 | private class StopRowPadLight { 19 | private final BooleanValue mStop; 20 | private final BooleanValue mExists; 21 | public StopRowPadLight(LaunchpadXSurface surface, Track track) { 22 | mStop = track.isStopped(); 23 | mExists = track.exists(); 24 | 25 | mStop.addValueObserver(s -> redraw(surface)); 26 | mExists.addValueObserver(e -> redraw(surface)); 27 | } 28 | 29 | public void draw(MultiStateHardwareLight light) { 30 | if(mExists.get()) { 31 | if(mStop.get()) { 32 | light.state().setValue(PadLightState.solidLight(7)); 33 | } else { 34 | light.state().setValue(PadLightState.solidLight(5)); 35 | } 36 | } else { 37 | light.setColor(Color.nullColor()); 38 | } 39 | } 40 | } 41 | 42 | public StopClipMixer(AtomicReference mixerMode, ControllerHost host, Transport transport, 43 | LaunchpadXSurface surface, TrackBank bank, AtomicBoolean launchAlt) { 44 | super(mixerMode, host, transport, surface, bank, Mode.MIXER_STOP, 5, launchAlt); 45 | 46 | for(int i = 0; i < 8; i++) { 47 | Track track = bank.getItemAt(i); 48 | mStopPads[i] = new StopRowPadLight(surface, track); 49 | mStopAction[i] = track.stopAction(); 50 | } 51 | } 52 | 53 | @Override 54 | public void onDraw(LaunchpadXSurface surface) { 55 | super.onDraw(surface); 56 | 57 | drawMixerModeIndicator(surface, 4); 58 | 59 | NoteButton[] finalRow = getFinalRow(surface); 60 | for(int i = 0; i < finalRow.length; i++) { 61 | mStopPads[i].draw(finalRow[i].light()); 62 | } 63 | } 64 | 65 | @Override 66 | public List onBind(LaunchpadXSurface surface) { 67 | List list = super.onBind(surface); 68 | 69 | NoteButton[] finalRow = getFinalRow(surface); 70 | for(int i = 0; i < finalRow.length; i++) { 71 | NoteButton pad = finalRow[i]; 72 | list.add(pad.button().pressedAction().addBinding(mStopAction[i])); 73 | } 74 | 75 | return list; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/mixer/VolumeMixer.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.mixer; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Mode; 5 | import io.github.jengamon.novation.modes.session.ArrowPadLight; 6 | import io.github.jengamon.novation.modes.session.TrackColorFaderLight; 7 | import io.github.jengamon.novation.surface.Fader; 8 | import io.github.jengamon.novation.surface.LaunchpadXPad; 9 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | public class VolumeMixer extends AbstractFaderMixerMode { 15 | private final TrackColorFaderLight[] faderLights = new TrackColorFaderLight[8]; 16 | private final Parameter[] volumes = new Parameter[8]; 17 | 18 | private final ArrowPadLight trackForwardLight; 19 | private final ArrowPadLight trackBackwardLight; 20 | private final HardwareActionBindable trackForwardAction; 21 | private final HardwareActionBindable trackBackwardAction; 22 | 23 | 24 | public VolumeMixer(AtomicReference mixerMode, ControllerHost host, Transport transport, 25 | LaunchpadXSurface surface, TrackBank bank) { 26 | super(mixerMode, host, transport, surface, Mode.MIXER_VOLUME, 64); 27 | 28 | for(int i = 0; i < 8; i++) { 29 | Track track = bank.getItemAt(i); 30 | 31 | faderLights[i] = new TrackColorFaderLight(surface, track, this::redraw); 32 | volumes[i] = track.volume(); 33 | } 34 | 35 | trackForwardLight = new ArrowPadLight(surface, bank.canScrollForwards(), mModeColor, this::redraw); 36 | trackBackwardLight = new ArrowPadLight(surface, bank.canScrollBackwards(), mModeColor, this::redraw); 37 | trackForwardAction = bank.scrollForwardsAction(); 38 | trackBackwardAction = bank.scrollBackwardsAction(); 39 | } 40 | 41 | private LaunchpadXPad getBack(LaunchpadXSurface surface) { return surface.left(); } 42 | private LaunchpadXPad getForward(LaunchpadXSurface surface) { return surface.right(); } 43 | 44 | @Override 45 | public void onDraw(LaunchpadXSurface surface) { 46 | super.onDraw(surface); 47 | 48 | drawMixerModeIndicator(surface, 0); 49 | 50 | Fader[] faders = surface.faders(); 51 | for(int i = 0; i < faders.length; i++) { 52 | faderLights[i].draw(faders[i].light()); 53 | } 54 | 55 | LaunchpadXPad back = getBack(surface); 56 | LaunchpadXPad frwd = getForward(surface); 57 | trackBackwardLight.draw(back.light()); 58 | trackForwardLight.draw(frwd.light()); 59 | } 60 | 61 | @Override 62 | public List onBind(LaunchpadXSurface surface) { 63 | List list = super.onBind(surface); 64 | 65 | // Enable faders 66 | surface.setupFaders(true, false, 21); 67 | 68 | LaunchpadXPad back = getBack(surface); 69 | LaunchpadXPad frwd = getForward(surface); 70 | list.add(back.button().pressedAction().addBinding(trackBackwardAction)); 71 | list.add(frwd.button().pressedAction().addBinding(trackForwardAction)); 72 | 73 | for(int i = 0; i < 8; i++) { 74 | Fader volumeFader = surface.faders()[i]; 75 | list.add(volumes[i].addBinding(volumeFader.fader())); 76 | } 77 | 78 | return list; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/session/ArrowPadLight.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.session; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.BooleanValue; 5 | import com.bitwig.extension.controller.api.MultiStateHardwareLight; 6 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 7 | import io.github.jengamon.novation.surface.state.PadLightState; 8 | 9 | import java.util.function.Consumer; 10 | 11 | public class ArrowPadLight { 12 | private final BooleanValue mIsValid; 13 | private final int mColor; 14 | public ArrowPadLight(LaunchpadXSurface surface, BooleanValue isValid, int color, Consumer redraw) { 15 | mIsValid = isValid; 16 | mColor = color; 17 | 18 | mIsValid.addValueObserver(v -> redraw.accept(surface)); 19 | } 20 | 21 | public ArrowPadLight(LaunchpadXSurface surface, BooleanValue isValid, Consumer redraw) { 22 | mIsValid = isValid; 23 | mColor = 84; 24 | 25 | mIsValid.addValueObserver(v -> redraw.accept(surface)); 26 | } 27 | 28 | public void draw(MultiStateHardwareLight arrowLight) { 29 | if(mIsValid.get()) { 30 | arrowLight.state().setValue(PadLightState.solidLight(mColor)); 31 | } else { 32 | arrowLight.setColor(Color.nullColor()); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/session/SessionPadLight.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.session; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Utils; 5 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 6 | import io.github.jengamon.novation.surface.state.PadLightState; 7 | 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | import java.util.function.Consumer; 11 | 12 | public class SessionPadLight { 13 | private final RangedValue mBPM; 14 | private final BooleanValue mArmed; 15 | private final BooleanValue mExists; 16 | private final BooleanValue mHasContent; 17 | private final ColorValue mColor; 18 | 19 | private final int mSlotIndex; 20 | 21 | private static class SlotState { 22 | public AtomicInteger mStateIndex; 23 | public AtomicBoolean mIsQueued; 24 | 25 | public SlotState() { 26 | mStateIndex = new AtomicInteger(0); 27 | mIsQueued = new AtomicBoolean(false); 28 | } 29 | } 30 | 31 | private final SlotState[] mSlotStates = new SlotState[8]; 32 | 33 | private enum State { 34 | STOPPED, 35 | PLAYING, 36 | RECORDING, 37 | QUEUE_STOP, 38 | QUEUE_PLAY, 39 | QUEUE_RECORD 40 | } 41 | 42 | public SessionPadLight(LaunchpadXSurface surface, ClipLauncherSlot slot, Track track, RangedValue bpm, Consumer redraw, int index) { 43 | mBPM = bpm; 44 | mArmed = track.arm(); 45 | mHasContent = slot.hasContent(); 46 | mColor = slot.color(); 47 | mExists = slot.exists(); 48 | mSlotIndex = index; 49 | 50 | // Also refresh whenever a slot's *existence* value changes ig... 51 | mHasContent.addValueObserver(ae -> redraw.accept(surface)); 52 | mBPM.addValueObserver(b -> redraw.accept(surface)); 53 | mArmed.addValueObserver(a -> redraw.accept(surface)); 54 | mExists.addValueObserver(e -> redraw.accept(surface)); 55 | mColor.addValueObserver((r, g, b) -> redraw.accept(surface)); 56 | 57 | for(int i = 0; i < 8; i++) { 58 | mSlotStates[i] = new SlotState(); 59 | } 60 | 61 | slot.sceneIndex().addValueObserver(si -> redraw.accept(surface)); 62 | track.clipLauncherSlotBank().addPlaybackStateObserver((slotIndex, state, isQueued) -> { 63 | SlotState slotState = mSlotStates[slotIndex]; 64 | slotState.mStateIndex.set(state); 65 | slotState.mIsQueued.set(isQueued); 66 | redraw.accept(surface); 67 | }); 68 | } 69 | 70 | /** 71 | * Because Bitwig basically fucks us over with non-descriptive/buggy APIs, we have to do this. (???) 72 | * Calculates the state of a button, given the last updated state and isQueued values of *all* slots. 73 | * @return the current state the button should be in. 74 | */ 75 | private State getState() { 76 | int state = mSlotStates[mSlotIndex].mStateIndex.get(); 77 | boolean isQueued = mSlotStates[mSlotIndex].mIsQueued.get(); 78 | if (state == 0) { 79 | return (isQueued ? State.QUEUE_STOP : State.STOPPED); 80 | } else if (state == 1) { 81 | return (isQueued ? State.QUEUE_PLAY : State.PLAYING); 82 | } else if (state == 2) { 83 | return (isQueued ? State.QUEUE_RECORD : State.RECORDING); 84 | } else { 85 | throw new RuntimeException("Invalid state " + state); 86 | } 87 | } 88 | 89 | public void draw(MultiStateHardwareLight slotLight) { 90 | byte pulseColor = (byte)0; 91 | byte blinkColor = (byte)0; 92 | byte solidColor = (byte)0; 93 | 94 | State mState = getState(); 95 | 96 | byte slotColor = Utils.toNovation(mColor.get()); 97 | 98 | if(mExists.get() && mHasContent.get()) { 99 | solidColor = slotColor; 100 | switch(mState) { 101 | case PLAYING: 102 | pulseColor = slotColor; 103 | break; 104 | case RECORDING: 105 | blinkColor = 6; 106 | break; 107 | case QUEUE_PLAY: 108 | blinkColor = 0x19; 109 | break; 110 | case QUEUE_STOP: 111 | blinkColor = 5; 112 | break; 113 | case QUEUE_RECORD: 114 | pulseColor = 6; 115 | break; 116 | case STOPPED: 117 | break; 118 | } 119 | } else { 120 | if(mArmed.get() && mExists.get()) { 121 | solidColor = 0x7; 122 | } 123 | 124 | switch(mState) { 125 | case QUEUE_RECORD: 126 | pulseColor = 6; 127 | break; 128 | case RECORDING: 129 | case PLAYING: 130 | case STOPPED: 131 | case QUEUE_PLAY: 132 | case QUEUE_STOP: 133 | break; 134 | } 135 | } 136 | 137 | slotLight.state().setValue(new PadLightState(mBPM.getRaw(), solidColor, blinkColor, pulseColor)); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/modes/session/TrackColorFaderLight.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.modes.session; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.surface.LaunchpadXSurface; 6 | import io.github.jengamon.novation.surface.state.FaderLightState; 7 | 8 | import java.util.function.Consumer; 9 | 10 | public class TrackColorFaderLight { 11 | private final BooleanValue mValid; 12 | private final ColorValue mColor; 13 | 14 | public TrackColorFaderLight(LaunchpadXSurface surface, Track track, Consumer redraw) { 15 | mValid = track.exists(); 16 | mColor = track.color(); 17 | 18 | mValid.addValueObserver(v -> redraw.accept(surface)); 19 | mColor.addValueObserver((r, g, b) -> redraw.accept(surface)); 20 | } 21 | 22 | public TrackColorFaderLight(LaunchpadXSurface surface, Send send, Consumer redraw) { 23 | mValid = send.exists(); 24 | mColor = send.sendChannelColor(); 25 | 26 | mValid.addValueObserver(v -> redraw.accept(surface)); 27 | mColor.addValueObserver((r, g, b) -> redraw.accept(surface)); 28 | } 29 | 30 | public void draw(MultiStateHardwareLight faderLight) { 31 | if(mValid.get()) { 32 | Color color = mColor.get(); 33 | 34 | if(color.getRed() + color.getBlue() + color.getGreen() == 0.0) { 35 | faderLight.state().setValue(new FaderLightState((byte)1)); 36 | } else { 37 | faderLight.setColor(mColor.get()); 38 | } 39 | } else { 40 | faderLight.setColor(Color.nullColor()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/surface/CCButton.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.surface; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Utils; 5 | import io.github.jengamon.novation.internal.ChannelType; 6 | import io.github.jengamon.novation.internal.Session; 7 | import io.github.jengamon.novation.surface.state.PadLightState; 8 | 9 | /** 10 | * Represents a pad that communicates with the device over CC. 11 | */ 12 | public class CCButton extends LaunchpadXPad { 13 | private final HardwareButton mButton; 14 | private final MultiStateHardwareLight mLight; 15 | private final int mCC; 16 | 17 | public CCButton(Session session, HardwareSurface surface, String name, int cc, double x, double y) { 18 | mCC = cc; 19 | mButton = surface.createHardwareButton(name); 20 | 21 | mLight = surface.createMultiStateHardwareLight("L" + name); 22 | mButton.setBackgroundLight(mLight); 23 | mButton.setBounds(x, y, 21, 21); 24 | 25 | mLight.state().onUpdateHardware(state -> { 26 | PadLightState padState = (PadLightState)state; 27 | if(padState != null) { 28 | session.sendMidi(0xB0, cc, padState.solid()); 29 | if(padState.blink() > 0) session.sendMidi(0xB1, cc, padState.blink()); 30 | if(padState.pulse() > 0) session.sendMidi(0xB2, cc, padState.pulse()); 31 | } 32 | }); 33 | mLight.setColorToStateFunction(color -> PadLightState.solidLight(Utils.toNovation(color))); 34 | 35 | MidiIn in = session.midiIn(ChannelType.DAW); 36 | 37 | // HardwareActionMatcher onPress = in.createActionMatcher("status == 0xB0 && data2 == 127 && data1 == " + cc); 38 | HardwareActionMatcher onRelease = in.createCCActionMatcher(0, cc, 0); 39 | AbsoluteHardwareValueMatcher onVelocity = in.createAbsoluteValueMatcher("status == 0xB0 && data2 > 0 && data1 == " + cc, "data2", 7); 40 | 41 | // mButton.pressedAction().setActionMatcher(onPress); 42 | mButton.pressedAction().setPressureActionMatcher(onVelocity); 43 | mButton.releasedAction().setActionMatcher(onRelease); 44 | } 45 | 46 | @Override 47 | public HardwareButton button() { return mButton; } 48 | 49 | @Override 50 | public AbsoluteHardwareKnob aftertouch() { 51 | return null; 52 | } 53 | 54 | @Override 55 | public MultiStateHardwareLight light() { return mLight; } 56 | 57 | @Override 58 | public int id() { 59 | return mCC; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/surface/Fader.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.surface; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.*; 5 | import io.github.jengamon.novation.Utils; 6 | import io.github.jengamon.novation.internal.ChannelType; 7 | import io.github.jengamon.novation.internal.Session; 8 | import io.github.jengamon.novation.surface.state.FaderLightState; 9 | 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | public class Fader { 13 | private final HardwareSlider mFader; 14 | private final MultiStateHardwareLight mLight; 15 | private final AtomicInteger mCC = new AtomicInteger(0); 16 | 17 | private final MidiIn mIn; 18 | 19 | public Fader(Session session, HardwareSurface surface, String name, double x, double y) { 20 | mFader = surface.createHardwareSlider(name); 21 | mLight = surface.createMultiStateHardwareLight("L" + name); 22 | 23 | mFader.setBackgroundLight(mLight); 24 | mLight.state().onUpdateHardware(state -> { 25 | FaderLightState faderState = (FaderLightState)state; 26 | if(faderState != null) { 27 | session.sendMidi(0xB5, mCC.get(), faderState.solid()); 28 | } 29 | }); 30 | mLight.setColorToStateFunction(color -> new FaderLightState(Utils.toNovation(color))); 31 | 32 | BooleanValue isUpdating = mFader.isUpdatingTargetValue(); 33 | isUpdating.markInterested(); 34 | mFader.targetValue().addValueObserver(tv -> { 35 | boolean didUpdate = isUpdating.get(); 36 | // System.out.println("DU>" + didUpdate); 37 | if(!didUpdate) { 38 | session.sendMidi(0xB4, mCC.get(), (int) Math.round(tv * 127)); 39 | } 40 | }); 41 | 42 | mFader.setBounds(x, y, 10, 23); 43 | mIn = session.midiIn(ChannelType.DAW); 44 | } 45 | 46 | public void resetColor() { 47 | mLight.setColor(Color.nullColor()); 48 | } 49 | 50 | public int id() { return mCC.get(); } 51 | public void setId(int cc) { 52 | mCC.set(cc); 53 | AbsoluteHardwareValueMatcher faderChange = mIn.createAbsoluteCCValueMatcher(4, cc); 54 | mFader.setAdjustValueMatcher(faderChange); 55 | } 56 | public HardwareSlider fader() { return mFader; } 57 | public MultiStateHardwareLight light() { return mLight; } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/surface/LaunchpadXPad.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.surface; 2 | 3 | import com.bitwig.extension.controller.api.AbsoluteHardwareKnob; 4 | import com.bitwig.extension.controller.api.HardwareButton; 5 | import com.bitwig.extension.controller.api.MultiStateHardwareLight; 6 | import io.github.jengamon.novation.surface.state.PadLightState; 7 | 8 | public abstract class LaunchpadXPad { 9 | public abstract HardwareButton button(); 10 | public abstract AbsoluteHardwareKnob aftertouch(); 11 | public abstract MultiStateHardwareLight light(); 12 | public abstract int id(); 13 | 14 | public void resetColor() { 15 | light().state().setValue(PadLightState.solidLight(0)); 16 | } 17 | 18 | public void updateBPM(double newBPM) { 19 | PadLightState state = (PadLightState)light().state().currentValue(); 20 | if(state != null) { 21 | PadLightState newState = new PadLightState(newBPM, state.solid(), state.blink(), state.pulse()); 22 | light().state().setValue(newState); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/surface/LaunchpadXSurface.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.surface; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Utils; 5 | import io.github.jengamon.novation.internal.ChannelType; 6 | import io.github.jengamon.novation.internal.Session; 7 | 8 | import java.util.Arrays; 9 | 10 | public class LaunchpadXSurface { 11 | private final CCButton mUpArrow; 12 | private final CCButton mDownArrow; 13 | private final CCButton mLeftArrow; 14 | private final CCButton mRightArrow; 15 | private final CCButton mSessionButton; 16 | private final CCButton mNoteButton; 17 | private final CCButton mCustomButton; 18 | private final CCButton mRecordButton; 19 | private final CCButton mNovationButton; 20 | private final AbsoluteHardwareControl mChannelPressure; 21 | private final Session mSession; 22 | private final HardwareSurface mSurface; 23 | 24 | private final CCButton[] mSceneButtons; 25 | 26 | private final NoteButton[][] mNoteButtons; 27 | 28 | private final Fader[] mFaders; 29 | 30 | //private int[] mVolumeFaderCCs = new int[]{21, 22, 23, 24, 25, 26, 27, 28}; 31 | //private int[] mFaderCCs = new int[]{45, 46, 47, 48, 49, 50, 51, 52}; 32 | 33 | public LaunchpadXPad up() { return mUpArrow; } 34 | public LaunchpadXPad down() { return mDownArrow; } 35 | public LaunchpadXPad left() { return mLeftArrow; } 36 | public LaunchpadXPad right() { return mRightArrow; } 37 | public LaunchpadXPad[] arrows() { return new LaunchpadXPad[] {mUpArrow, mDownArrow, mLeftArrow, mRightArrow}; } 38 | 39 | public LaunchpadXPad session() { return mSessionButton; } 40 | public LaunchpadXPad note() { return mNoteButton; } 41 | public LaunchpadXPad custom() { return mCustomButton; } 42 | 43 | public LaunchpadXPad record() { return mRecordButton; } 44 | public LaunchpadXPad novation() { return mNovationButton; } 45 | 46 | public LaunchpadXPad[] scenes() { return mSceneButtons; } 47 | public NoteButton[][] notes() { return mNoteButtons; } 48 | public AbsoluteHardwareControl channelPressure() { return mChannelPressure; } 49 | 50 | public Fader[] faders() { return mFaders; } 51 | 52 | public LaunchpadXSurface(ControllerHost host, Session session, HardwareSurface surface) { 53 | mUpArrow = new CCButton(session, surface, "Up", 91, 13, 13); 54 | mDownArrow = new CCButton(session, surface, "Down", 92, 13 + 23, 13); 55 | mLeftArrow = new CCButton(session, surface, "Left", 93, 13 + 23*2, 13); 56 | mRightArrow = new CCButton(session, surface, "Right", 94, 13 + 23*3, 13); 57 | mSessionButton = new CCButton(session, surface, "Session", 95, 13 + 23*4, 13); 58 | mNoteButton = new CCButton(session, surface, "Note", 96, 13 + 23*5, 13); 59 | mCustomButton = new CCButton(session, surface, "Custom", 97, 13+23*6, 13); 60 | mRecordButton = new CCButton(session, surface, "Record", 98, 13 + 23*7, 13); 61 | mNovationButton = new CCButton(session, surface, "N", 99, 13 + 23 * 8, 13); 62 | mSceneButtons = new CCButton[8]; 63 | 64 | mChannelPressure = surface.createAbsoluteHardwareKnob("Pressure"); 65 | MidiIn in = session.midiIn(ChannelType.DAW); 66 | AbsoluteHardwareValueMatcher onDrumChannelPressure = in.createAbsoluteValueMatcher("status == 0xD8", "data1", 7); 67 | mChannelPressure.setAdjustValueMatcher(onDrumChannelPressure); 68 | 69 | mSession = session; 70 | mSurface = surface; 71 | 72 | for(int i = 0; i < 8; i++) { 73 | mSceneButtons[i] = new CCButton(session, surface, "S" + (i+1), (8 - i) * 10 + 9, 13 + 23 * 8, 13 + 23 * (1 + i)); 74 | } 75 | 76 | mNoteButtons = new NoteButton[8][8]; 77 | int[] row_offsets = new int[]{80, 70, 60, 50, 40, 30, 20, 10}; 78 | int[] drum_pad_notes = new int[] { 79 | 64, 65, 66, 67, 96, 97, 98, 99, 80 | 60, 61, 62, 63, 92, 93, 94, 95, 81 | 56, 57, 58, 59, 88, 89, 90, 91, 82 | 52, 53, 54, 55, 84, 85, 86, 87, 83 | 48, 49, 50, 51, 80, 81, 82, 83, 84 | 44, 45, 46, 47, 76, 77, 78, 79, 85 | 40, 41, 42, 43, 72, 73, 74, 75, 86 | 36, 37, 38, 39, 68, 69, 70, 71 87 | }; 88 | for(int row = 0; row < 8; row++) { 89 | mNoteButtons[row] = new NoteButton[8]; 90 | for(int col = 0; col < 8; col++) { 91 | mNoteButtons[row][col] = new NoteButton(host, session, surface, row + "," + col, row_offsets[row] + col + 1, drum_pad_notes[row * 8 + col], 13 + (col * 23), 13 + 23 + (row * 23)); 92 | } 93 | } 94 | 95 | mFaders = new Fader[8]; 96 | for(int i = 0; i < 8; i++) { 97 | mFaders[i] = new Fader(session, surface, "FV" + i,0, i * 24); 98 | } 99 | } 100 | 101 | public void setupFaders(boolean vertical, boolean bipolar, int baseCC) { 102 | boolean[] bipolars = new boolean[8]; 103 | Arrays.fill(bipolars, bipolar); 104 | setupFaders(vertical, bipolars, baseCC); 105 | } 106 | 107 | public void setupFaders(boolean vertical, boolean[] bipolar, int baseCC) { 108 | StringBuilder sysexString = new StringBuilder(); 109 | sysexString.append("01 00"); 110 | if(vertical) { 111 | sysexString.append("00 "); 112 | } else { 113 | sysexString.append("01 "); 114 | } 115 | // assert colors.length == 8; 116 | for(int i = 0; i < 8; i++) { 117 | int cc = baseCC + i; 118 | sysexString.append(Utils.toHexString((byte)i)); 119 | if(bipolar[i]) { 120 | sysexString.append("01"); 121 | } else { 122 | sysexString.append("00"); 123 | } 124 | sysexString.append(Utils.toHexString((byte)cc)); 125 | // sysexString.append(Utils.toHexString((byte)colors[i])); 126 | sysexString.append("00 "); 127 | } 128 | mSurface.invalidateHardwareOutputState(); 129 | mSession.sendSysex(sysexString.toString()); 130 | 131 | for(int i = 0; i < 8; i++) { 132 | mFaders[i].setId(baseCC + i); 133 | } 134 | } 135 | 136 | /** 137 | * Clears all color states for the surface. 138 | */ 139 | public void clear() { 140 | mUpArrow.resetColor(); 141 | mDownArrow.resetColor(); 142 | mLeftArrow.resetColor(); 143 | mRightArrow.resetColor(); 144 | 145 | for(LaunchpadXPad scenePad : mSceneButtons) { 146 | scenePad.resetColor(); 147 | } 148 | 149 | for(LaunchpadXPad[] noteRow : mNoteButtons) { 150 | for(LaunchpadXPad note : noteRow) { 151 | note.resetColor(); 152 | } 153 | } 154 | 155 | for(Fader fader : mFaders) { 156 | fader.resetColor(); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/surface/NoteButton.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.surface; 2 | 3 | import com.bitwig.extension.controller.api.*; 4 | import io.github.jengamon.novation.Utils; 5 | import io.github.jengamon.novation.internal.ChannelType; 6 | import io.github.jengamon.novation.internal.Session; 7 | import io.github.jengamon.novation.surface.state.PadLightState; 8 | 9 | /** 10 | * Represents a pad the communicates with NoteOn events 11 | * These are slightly more complicated than the CC button devices as they have 2 notes, 12 | * a drum pad mode note and a session view note 13 | */ 14 | public class NoteButton extends LaunchpadXPad { 15 | private final HardwareButton mButton; 16 | private final AbsoluteHardwareKnob mAftertouch; 17 | 18 | private final MultiStateHardwareLight mLight; 19 | private final int mNote; 20 | private final int mDPNote; 21 | 22 | public NoteButton(ControllerHost host, Session session, HardwareSurface surface, String name, int note, int dpnote, double x, double y) { 23 | mNote = note; 24 | mDPNote = dpnote; 25 | 26 | mButton = surface.createHardwareButton(name); 27 | mAftertouch = surface.createAbsoluteHardwareKnob("AFT" + name); 28 | 29 | mLight = surface.createMultiStateHardwareLight("L" + name); 30 | mButton.setBackgroundLight(mLight); 31 | mButton.setBounds(x, y, 21, 21); 32 | mButton.setLabel(" "); // Don't label note pads 33 | 34 | // Upload the state to the hardware 35 | mLight.state().onUpdateHardware(state -> { 36 | PadLightState padState = (PadLightState)state; 37 | if(padState != null) { 38 | session.sendMidi(0x90, note, padState.solid()); 39 | session.sendMidi(0x98, dpnote, padState.solid()); 40 | if(padState.blink() > 0) session.sendMidi(0x91, note, padState.blink()); 41 | if(padState.blink() > 0) session.sendMidi(0x99, dpnote, padState.blink()); 42 | if(padState.pulse() > 0) session.sendMidi(0x92, note, padState.pulse()); 43 | if(padState.pulse() > 0) session.sendMidi(0x9A, dpnote, padState.pulse()); 44 | } 45 | }); 46 | mLight.setColorToStateFunction(color -> PadLightState.solidLight(Utils.toNovation(color))); 47 | 48 | MidiIn in = session.midiIn(ChannelType.DAW); 49 | 50 | // String expr = host.midiExpressions().createIsNoteOnExpression(0, note); 51 | String aftExpr = host.midiExpressions().createIsPolyAftertouch(0, note); 52 | // String drumExpr = host.midiExpressions().createIsNoteOnExpression(8, dpnote); 53 | String drumAftExpr = host.midiExpressions().createIsPolyAftertouch(8, dpnote); 54 | 55 | // System.out.println(drumExpr); 56 | // System.out.println(drumAftExpr); 57 | 58 | HardwareActionMatcher onRelease = in.createActionMatcher("status == 0x90 && data2 == 0 && data1 == " + note); 59 | AbsoluteHardwareValueMatcher onAfterRelease = in.createAbsoluteValueMatcher(aftExpr + " && data2 == 0", "data2", 8); 60 | AbsoluteHardwareValueMatcher onVelocity = in.createNoteOnVelocityValueMatcher(0, note); 61 | AbsoluteHardwareValueMatcher onAftertouch = in.createPolyAftertouchValueMatcher(0, note); 62 | HardwareActionMatcher onDrumRelease = in.createActionMatcher("status == 0x98 && data1 == " + dpnote + " && data2 == 0"); 63 | AbsoluteHardwareValueMatcher onDrumPolyAfterOff = in.createAbsoluteValueMatcher( drumAftExpr + " && data2 == 0", "data2", 8); 64 | AbsoluteHardwareValueMatcher onDrumVelocity = in.createNoteOnVelocityValueMatcher(8, dpnote); 65 | AbsoluteHardwareValueMatcher onDrumAftertouch = in.createPolyAftertouchValueMatcher(8, dpnote); 66 | 67 | mButton.setAftertouchControl(mAftertouch); 68 | mAftertouch.setAdjustValueMatcher( 69 | host.createOrAbsoluteHardwareValueMatcher(onAftertouch, onDrumAftertouch) 70 | ); 71 | mButton.pressedAction().setPressureActionMatcher( 72 | host.createOrAbsoluteHardwareValueMatcher(onVelocity, onDrumVelocity) 73 | ); 74 | mButton.releasedAction().setActionMatcher( 75 | host.createOrHardwareActionMatcher(onRelease, onDrumRelease) 76 | ); 77 | mButton.releasedAction().setPressureActionMatcher( 78 | host.createOrAbsoluteHardwareValueMatcher(onAfterRelease, onDrumPolyAfterOff) 79 | ); 80 | } 81 | 82 | @Override 83 | public HardwareButton button() { 84 | return mButton; 85 | } 86 | 87 | @Override 88 | public AbsoluteHardwareKnob aftertouch() { 89 | return mAftertouch; 90 | } 91 | 92 | @Override 93 | public MultiStateHardwareLight light() { 94 | return mLight; 95 | } 96 | 97 | @Override 98 | public int id() { 99 | return mNote; 100 | } 101 | 102 | public int drum_id() { 103 | return mDPNote; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/surface/state/FaderLightState.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.surface.state; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.HardwareLightVisualState; 5 | import com.bitwig.extension.controller.api.InternalHardwareLightState; 6 | import io.github.jengamon.novation.Utils; 7 | 8 | public class FaderLightState extends InternalHardwareLightState { 9 | private final byte mSolid; 10 | 11 | public FaderLightState(byte solid) { 12 | mSolid = (solid < 0 ? 0 : solid); 13 | } 14 | 15 | @Override 16 | public HardwareLightVisualState getVisualState() { 17 | Color solidColor = Utils.fromNovation(mSolid); 18 | 19 | return HardwareLightVisualState.createForColor(solidColor); 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if(o != null && o.getClass() == FaderLightState.class) { 25 | FaderLightState other = (FaderLightState)o; 26 | return mSolid == other.mSolid; 27 | } else { 28 | return false; 29 | } 30 | } 31 | 32 | public byte solid() { return mSolid; } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/github/jengamon/novation/surface/state/PadLightState.java: -------------------------------------------------------------------------------- 1 | package io.github.jengamon.novation.surface.state; 2 | 3 | import com.bitwig.extension.api.Color; 4 | import com.bitwig.extension.controller.api.HardwareLightVisualState; 5 | import com.bitwig.extension.controller.api.InternalHardwareLightState; 6 | import io.github.jengamon.novation.Utils; 7 | 8 | public class PadLightState extends InternalHardwareLightState { 9 | private final byte mSolid; 10 | private final byte mPulse; 11 | private final byte mBlink; 12 | private final double mBPM; 13 | 14 | public PadLightState(double bpm, byte solid, byte blink, byte pulse) { 15 | mBPM = bpm; 16 | mSolid = (solid < 0 ? 0 : solid); 17 | mBlink = (blink < 0 ? 0 : blink); 18 | mPulse = (pulse < 0 ? 0 : pulse); 19 | } 20 | 21 | public static PadLightState solidLight(int color) { 22 | return new PadLightState(1.0, (byte)color, (byte)0, (byte)0); 23 | } 24 | 25 | public static PadLightState pulseLight(double bpm, int color) { 26 | return new PadLightState(bpm, (byte)0, (byte)0, (byte)color); 27 | } 28 | 29 | @Override 30 | public HardwareLightVisualState getVisualState() { 31 | if(mPulse > 0) { 32 | Color pulseColor = Utils.fromNovation(mPulse); 33 | return HardwareLightVisualState.createBlinking( 34 | pulseColor, 35 | Color.mix(pulseColor, Color.nullColor(), 0.7), 36 | 60.0 / mBPM, 37 | 60.0 / mBPM 38 | ); 39 | } 40 | 41 | Color solidColor = Utils.fromNovation(mSolid); 42 | if(mBlink > 0) { 43 | Color blinkColor = Utils.fromNovation(mBlink); 44 | return HardwareLightVisualState.createBlinking( 45 | blinkColor, 46 | solidColor, 47 | 60.0 / mBPM, 48 | 60.0 / mBPM 49 | ); 50 | } 51 | 52 | return HardwareLightVisualState.createForColor(solidColor); 53 | } 54 | 55 | @Override 56 | public boolean equals(Object o) { 57 | if(o != null && o.getClass() == PadLightState.class) { 58 | PadLightState other = (PadLightState)o; 59 | return mSolid == other.mSolid && mPulse == other.mPulse && mBlink == other.mBlink && mBPM == other.mBPM; 60 | } else { 61 | return false; 62 | } 63 | } 64 | 65 | public byte solid() { return mSolid; } 66 | public byte pulse() { return mPulse; } 67 | public byte blink() { return mBlink; } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition: -------------------------------------------------------------------------------- 1 | io.github.jengamon.novation.LaunchpadXExtensionDefinition --------------------------------------------------------------------------------