├── .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 |
4 |
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 |
11 |
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 | 
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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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
--------------------------------------------------------------------------------