diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/OverviewGrid.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ApcOverviewGrid.java similarity index 76% rename from src/main/java/com/bitwig/extensions/controllers/akai/apc64/OverviewGrid.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc64/ApcOverviewGrid.java index af2fd8c6..d199a1ad 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/OverviewGrid.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ApcOverviewGrid.java @@ -1,91 +1,91 @@ package com.bitwig.extensions.controllers.akai.apc64; -public class OverviewGrid { - +public class ApcOverviewGrid { + private int sceneOffset; private int trackOffset; private int numberOfScenes; private int numberOfTracks; - + private int trackPosition; private int scenePosition; - + private final int[][] hasClips = new int[8][8]; private final int[] sceneQueuedClips = new int[64]; - + public int getNumberOfScenes() { return numberOfScenes; } - + public void setNumberOfScenes(final int numberOfScenes) { this.numberOfScenes = numberOfScenes; } - + public int getNumberOfTracks() { return numberOfTracks; } - + public void setNumberOfTracks(final int numberOfTracks) { this.numberOfTracks = numberOfTracks; } - + public int getTrackPosition() { return trackPosition - trackOffset; } - + public int getTrackOffset() { return trackOffset; } - + public void setTrackPosition(final int trackPosition) { this.trackPosition = trackPosition; this.trackOffset = (trackPosition / 64) * 64; } - + public int getScenePosition() { return scenePosition - sceneOffset; } - + public void setScenePosition(final int scenePosition) { this.scenePosition = scenePosition; this.sceneOffset = (scenePosition / 64) * 64; } - + public int getSceneOffset() { return sceneOffset; } - - public void markSceneQueued(int sceneIndex, boolean isQueued) { + + public void markSceneQueued(final int sceneIndex, final boolean isQueued) { if (isQueued) { sceneQueuedClips[sceneIndex]++; } else if (sceneQueuedClips[sceneIndex] > 0) { sceneQueuedClips[sceneIndex]--; } } - - public void setHasClips(int trackIndex, int sceneIndex, boolean hasClip) { - int gridScene = (sceneIndex) / 8; - int gridTrack = (trackIndex) / 8; + + public void setHasClips(final int trackIndex, final int sceneIndex, final boolean hasClip) { + final int gridScene = (sceneIndex) / 8; + final int gridTrack = (trackIndex) / 8; if (hasClip) { this.hasClips[gridTrack][gridScene]++; } else if (this.hasClips[gridTrack][gridScene] > 0) { this.hasClips[gridTrack][gridScene]--; } } - - public boolean hasClips(int trackIndex, int sceneIndex) { + + public boolean hasClips(final int trackIndex, final int sceneIndex) { return this.hasClips[trackIndex][sceneIndex] > 0; } - - public boolean hasQueuedScenes(int sceneIndex) { - int index = sceneIndex - sceneOffset; + + public boolean hasQueuedScenes(final int sceneIndex) { + final int index = sceneIndex - sceneOffset; if (index > 63) { return false; } return this.sceneQueuedClips[sceneIndex - sceneOffset] > 0; } - - public boolean inGrid(int trackIndex, int sceneIndex) { + + public boolean inGrid(final int trackIndex, final int sceneIndex) { final int posX = trackIndex * 8; final int posY = sceneIndex * 8; return posX < (numberOfTracks - trackOffset) && posY < (numberOfScenes - sceneOffset); diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/FocusClip.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/FocusClip.java index 96acb796..906fc3a9 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/FocusClip.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/FocusClip.java @@ -1,42 +1,49 @@ package com.bitwig.extensions.controllers.akai.apc64; -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; -import com.bitwig.extensions.framework.di.Component; -import com.bitwig.extensions.framework.di.Inject; - import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.Clip; +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.ClipLauncherSlotBank; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Project; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.di.Inject; + @Component public class FocusClip { private static final int SINGLE_SLOT_RANGE = 8; - + private final CursorTrack cursorTrack; private final Application application; private final Transport transport; private final Clip mainCursorClip; private final Project project; private final ControllerHost host; - private final OverviewGrid overviewGrid; - + private final ApcOverviewGrid overviewGrid; + private int selectedSlotIndex = -1; private int scrollOffset = 0; - + private String currentTrackName = ""; - + private final Map indexMemory = new HashMap<>(); private final ClipLauncherSlotBank slotBank; private ClipLauncherSlot focusSlot; private Runnable scrollTask = null; - + @Inject private MidiProcessor midiProcessor; - - public FocusClip(ControllerHost host, Application application, Transport transport, ViewControl viewControl, - Project project) { + + public FocusClip(final ControllerHost host, final Application application, final Transport transport, + final ViewControl viewControl, final Project project) { this.cursorTrack = viewControl.getCursorTrack(); this.project = project; this.host = host; @@ -49,10 +56,10 @@ public FocusClip(ControllerHost host, Application application, Transport transpo slot.isPlaying().markInterested(); slot.hasContent().markInterested(); } - + this.application = application; this.transport = transport; - + slotBank.addPlaybackStateObserver((slotIndex, playbackState, isQueued) -> { if (playbackState != 0 && !isQueued) { slotBank.select(slotIndex); @@ -73,7 +80,7 @@ public FocusClip(ControllerHost host, Application application, Transport transpo scrollTask = null; } }); - + this.cursorTrack.name().addValueObserver(name -> { selectedSlotIndex = -1; currentTrackName = name; @@ -84,7 +91,7 @@ public FocusClip(ControllerHost host, Application application, Transport transpo }); mainCursorClip = viewControl.getCursorClip(); } - + public void invokeRecord() { if (selectedSlotIndex != -1) { final ClipLauncherSlot slot = slotBank.getItemAt(selectedSlotIndex); @@ -92,63 +99,65 @@ public void invokeRecord() { slot.launch(); transport.isClipLauncherOverdubEnabled().set(false); } else { - Optional emptySlot = getFirstEmptySlot(selectedSlotIndex); + final Optional emptySlot = getFirstEmptySlot(selectedSlotIndex); if (emptySlot.isPresent()) { recordAction(emptySlot.get()); } else { project.createScene(); host.scheduleTask( - () -> getFirstEmptySlot(selectedSlotIndex).ifPresent(newSlot -> recordAction(newSlot)), 50); + () -> getFirstEmptySlot(selectedSlotIndex).ifPresent(newSlot -> recordAction(newSlot)), 50); } } } else { getFirstEmptySlot(selectedSlotIndex).ifPresent(slot -> recordAction(slot)); } } - - private void recordAction(ClipLauncherSlot emptySlot) { + + private void recordAction(final ClipLauncherSlot emptySlot) { emptySlot.launch(); transport.isClipLauncherOverdubEnabled().set(true); } - + public void duplicateContent() { mainCursorClip.duplicateContent(); } - + public void quantize(final double amount) { mainCursorClip.quantize(amount); } - + public void clearSteps() { mainCursorClip.clearSteps(); } - + public void transpose(final int semitones) { mainCursorClip.transpose(semitones); } - - public void focusOnNextEmpty(Consumer postCreation) { + + public void focusOnNextEmpty(final Consumer postCreation) { if (focusSlotIsEmpty()) { postCreation.accept(focusSlot); } else { getFirstEmptySlot(selectedSlotIndex) // - .ifPresentOrElse(slot -> postCreation.accept(slot), // - () -> ensureEmptySlot(postCreation)); + .ifPresentOrElse( + slot -> postCreation.accept(slot), // + () -> ensureEmptySlot(postCreation)); } } - - private void ensureEmptySlot(Consumer postCreation) { + + private void ensureEmptySlot(final Consumer postCreation) { project.createScene(); - host.scheduleTask(() -> getFirstEmptySlot(selectedSlotIndex).ifPresent(newSlot -> postCreation.accept(newSlot)), - 50); + host.scheduleTask( + () -> getFirstEmptySlot(selectedSlotIndex).ifPresent(newSlot -> postCreation.accept(newSlot)), + 50); } - + private boolean focusSlotIsEmpty() { return focusSlot != null && !focusSlot.hasContent().get() && focusSlot.exists().get(); } - - private Optional getFirstEmptySlot(int startIndex) { - int start = startIndex < 0 ? 0 : startIndex; + + private Optional getFirstEmptySlot(final int startIndex) { + final int start = startIndex < 0 ? 0 : startIndex; for (int i = start; i < slotBank.getSizeOfBank(); i++) { final ClipLauncherSlot slot = slotBank.getItemAt(i); if (!slot.hasContent().get() && slot.exists().get()) { @@ -157,8 +166,8 @@ private Optional getFirstEmptySlot(int startIndex) { } return Optional.empty(); } - - public void clearNotes(int noteToClear) { + + public void clearNotes(final int noteToClear) { mainCursorClip.clearStepsAtY(0, noteToClear); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ViewControl.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ViewControl.java index 272bd7de..d17049da 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ViewControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ViewControl.java @@ -1,12 +1,17 @@ package com.bitwig.extensions.controllers.akai.apc64; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.Clip; +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extensions.controllers.akai.apc.common.led.ColorLookup; import com.bitwig.extensions.framework.di.Component; @Component public class ViewControl { - + private final TrackBank trackBank; private final TrackBank maxTrackBank; private final CursorTrack cursorTrack; @@ -16,15 +21,15 @@ public class ViewControl { private int selectedTrackIndex; private final int[] trackColors = new int[8]; private int cursorTrackColor = 0; - private final OverviewGrid overviewGrid = new OverviewGrid(); - + private final ApcOverviewGrid overviewGrid = new ApcOverviewGrid(); + public ViewControl(final ControllerHost host) { rootTrack = host.getProject().getRootTrackGroup(); trackBank = host.createTrackBank(8, 1, 8, true); maxTrackBank = host.createTrackBank(64, 1, 64, false); maxTrackBank.sceneBank().scrollPosition().markInterested(); maxTrackBank.scrollPosition().markInterested(); - + trackBank.sceneBank().itemCount().addValueObserver(overviewGrid::setNumberOfScenes); trackBank.channelCount().addValueObserver(overviewGrid::setNumberOfTracks); trackBank.scrollPosition().addValueObserver(pos -> { @@ -39,13 +44,13 @@ public ViewControl(final ControllerHost host) { maxTrackBank.sceneBank().scrollPosition().set(overviewGrid.getSceneOffset()); } }); - + cursorTrack = host.createCursorTrack(6, 128); trackBank.followCursorTrack(cursorTrack); cursorTrack.exists().markInterested(); for (int i = 0; i < 8; i++) { - int index = i; - Track track = trackBank.getItemAt(i); + final int index = i; + final Track track = trackBank.getItemAt(i); prepareTrack(track); track.color().addValueObserver((r, g, b) -> { trackColors[index] = ColorLookup.toColor(r, g, b); @@ -57,24 +62,24 @@ public ViewControl(final ControllerHost host) { }); } setUpFocusScene(); - + deviceControl = new DeviceControl(cursorTrack, rootTrack); cursorTrack.name().markInterested(); cursorClip = host.createLauncherCursorClip(32, 128); cursorClip.setStepSize(0.125); - + cursorTrack.color().addValueObserver((r, g, b) -> { this.cursorTrackColor = com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup.toColor(r, g, b); }); prepareTrack(cursorTrack); } - + private void setUpFocusScene() { for (int i = 0; i < 64; i++) { final int trackIndex = i; - Track track = maxTrackBank.getItemAt(trackIndex); + final Track track = maxTrackBank.getItemAt(trackIndex); for (int j = 0; j < 64; j++) { - int sceneIndex = j; + final int sceneIndex = j; final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); slot.hasContent().addValueObserver(hasContent -> { overviewGrid.setHasClips(trackIndex, sceneIndex, hasContent); @@ -85,26 +90,26 @@ private void setUpFocusScene() { } } } - - public int getTrackColor(int index) { + + public int getTrackColor(final int index) { return trackColors[index]; } - + public int getCursorTrackColor() { return cursorTrackColor; } - + public int getSelectedTrackIndex() { return selectedTrackIndex; } - + private void prepareTrack(final Track track) { track.arm().markInterested(); track.exists().markInterested(); track.solo().markInterested(); track.mute().markInterested(); } - + public void scrollToOverview(final int trackIndex, final int sceneIndex) { final int posX = trackIndex * 8 + overviewGrid.getTrackOffset(); final int posY = sceneIndex * 8 + overviewGrid.getSceneOffset(); @@ -113,58 +118,58 @@ public void scrollToOverview(final int trackIndex, final int sceneIndex) { trackBank.sceneBank().scrollPosition().set(posY); } } - + public boolean inOverviewGrid(final int trackIndex, final int sceneIndex) { return overviewGrid.inGrid(trackIndex, sceneIndex); } - + public boolean canScrollVertical(final int delta) { - int newPos = overviewGrid.getScenePosition() + delta; + final int newPos = overviewGrid.getScenePosition() + delta; return newPos >= 0 && newPos < overviewGrid.getNumberOfScenes(); } - - + + public boolean canScrollHorizontal(final int delta) { - int newPos = overviewGrid.getTrackPosition() + delta; + final int newPos = overviewGrid.getTrackPosition() + delta; return newPos >= 0 && newPos < overviewGrid.getNumberOfTracks(); } - + public boolean inOverviewGridFocus(final int trackIndex, final int sceneIndex) { final int locX = overviewGrid.getTrackPosition() / 8; final int locY = overviewGrid.getScenePosition() / 8; return locX == trackIndex && locY == sceneIndex; } - - + + public TrackBank getTrackBank() { return trackBank; } - + public CursorTrack getCursorTrack() { return cursorTrack; } - + public Track getRootTrack() { return rootTrack; } - + public Clip getCursorClip() { return cursorClip; } - - public OverviewGrid getOverviewGrid() { + + public ApcOverviewGrid getOverviewGrid() { return overviewGrid; } - + public DeviceControl getDeviceControl() { return deviceControl; } - - public boolean hasQueuedClips(int sceneIndex) { + + public boolean hasQueuedClips(final int sceneIndex) { return overviewGrid.hasQueuedScenes(sceneIndex); } - - public boolean hasClips(int trackIndex, int sceneIndex) { + + public boolean hasClips(final int trackIndex, final int sceneIndex) { return overviewGrid.hasClips(trackIndex, sceneIndex); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/BrowserLayer.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/BrowserLayer.java index 5b3bb276..1bd7079a 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/BrowserLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/BrowserLayer.java @@ -41,12 +41,12 @@ public class BrowserLayer extends Layer { private PinnableCursorDevice cursorDevice; private CursorTrack cursorTrack; private boolean enforceDeviceContent = true; - - private int encoderClickCount = 0; + private final ControllerHost host; public BrowserLayer(final Layers layers, final ControllerHost host) { super(layers, "BROWSER_LAYER"); browser = host.createPopupBrowser(); + this.host = host; browser.exists().addValueObserver(this::handleBrowserOpened); browser.contentTypeNames().addValueObserver(contentTypeNames -> this.contentTypeNames = contentTypeNames); resultCursorItem = (CursorBrowserResultItem) browser.resultsColumn().createCursorItem(); @@ -97,19 +97,16 @@ public void init(final ViewControl viewCursorControl, final HwElements hwElement this.addBinding(new MainViewDisplayBinding(contextPage, display, resultNameValue, categoryItem.name())); - this.bindPressed(encoderPress, () -> { - if (resultCursorItem.exists().get()) { - if (encoderClickCount == 0) { - display.sendPopup("Click again ", "to load", KeylabIcon.NONE); - encoderClickCount++; - } else { + this.bindPressed( + encoderPress, () -> { + if (resultCursorItem.exists().get()) { browser.commit(); - encoderClickCount = 0; + final String selected = resultNameValue.get(); + host.scheduleTask(() -> display.sendPopup("Preset selected ", selected, KeylabIcon.NONE), 400); + } else { + display.sendPopup("Nothing selected ", "", KeylabIcon.NONE); } - } else { - display.sendPopup("Nothing selected ", "", KeylabIcon.NONE); - } - }); + }); } private String getResultData(final boolean exists, final String resultName) { @@ -122,14 +119,16 @@ private void bindContextButtons() { final ContextPageConfiguration contextPage = mainScreenSection.getContextPage(); context1.bindPressed(navigationLayer, this::toggleBrowserAction); final FooterIconDisplayBinding footerIconBinding = - new FooterIconDisplayBinding(contextPage, display, browsingInitiated, 0, ContextPart.FrameType.FRAME_SMALL, + new FooterIconDisplayBinding( + contextPage, display, browsingInitiated, 0, ContextPart.FrameType.FRAME_SMALL, ContextPart.FrameType.BAR); navigationLayer.addBinding(footerIconBinding); context1.bindLight(navigationLayer, () -> this.isActive() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); final RgbButton context3 = mainScreenSection.getContextButton(2); final RgbButton context4 = mainScreenSection.getContextButton(3); context3.bindRepeatHold(this, categoryItem::selectPrevious, BUTTON_WAIT_UTIL_REPEAT, HOLD_REPEAT_FREQUENCY); - context3.bindLight(this, + context3.bindLight( + this, () -> categoryItem.hasPrevious().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); context4.bindRepeatHold(this, categoryItem::selectNext, BUTTON_WAIT_UTIL_REPEAT, HOLD_REPEAT_FREQUENCY); context4.bindLight(this, () -> categoryItem.hasNext().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); @@ -145,10 +144,6 @@ private void toggleBrowserAction() { } private void handleBrowserOpened(final boolean exists) { - // if (browsingInitiated.get()) { - // browser.shouldAudition().set(false); - // } - // driver.browserDisplayMode(exists); if (!exists) { browsingInitiated.set(false); this.setIsActive(false); @@ -161,11 +156,9 @@ private void mainEncoderAction(final int increment) { } else { resultCursorItem.selectPrevious(); } - encoderClickCount = 0; } private void openBrowser() { - encoderClickCount = 0; if (cursorDevice.exists().get()) { browsingInitiated.set(true); enforceDeviceContent = true; @@ -177,7 +170,6 @@ private void openBrowser() { } private void exitBrowser() { - encoderClickCount = 0; if (browser.exists().get()) { browser.cancel(); } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/CCAssignment.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/CCAssignment.java index 9abaa01e..ebb2ff38 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/CCAssignment.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/CCAssignment.java @@ -1,50 +1,50 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; public enum CCAssignment { - SAVE(40, 0x0C), - PUNCH(41, 0x0D),// - UNDO(42, 0x0E),// - REDO(43, 0x0F),// - LOOP(24, 0x10),// - RWD(25, 0x11),// - FFWD(26, 0x12),// - METRO(27, 0x13),// - STOP(20, 0x14),// - PLAY(21, 0x15),// - REC(22, 0x16),// - TAP(23, 0x17),// - PART(119, 0x07),// - CONTEXT1(44, 0x18),// - CONTEXT2(45, 0x19),// - CONTEXT3(46, 0x1A),// - CONTEXT4(47, 0x1B),// - PAD1_A(36, 0x1C, true),// - PAD1_B(44, 0x24, true),// - ; - - private final int ccId; - private final int itemId; - private final boolean isMultiBase; - - CCAssignment(final int ccId, final int itemId, final boolean isMultiBase) { - this.ccId = ccId; - this.itemId = itemId; - this.isMultiBase = isMultiBase; - } - - CCAssignment(final int ccId, final int itemId) { - this(ccId, itemId, false); - } - - public int getItemId() { - return itemId; - } - - public int getCcId() { - return ccId; - } - - public boolean isMultiBase() { - return isMultiBase; - } + SAVE(40, 0x0C), + QUANTIZE(41, 0x0D),// + UNDO(42, 0x0E),// + REDO(43, 0x0F),// + LOOP(24, 0x10),// + RWD(25, 0x11),// + FFWD(26, 0x12),// + METRO(27, 0x13),// + STOP(20, 0x14),// + PLAY(21, 0x15),// + REC(22, 0x16),// + TAP(23, 0x17),// + PART(119, 0x07),// + CONTEXT1(44, 0x18),// + CONTEXT2(45, 0x19),// + CONTEXT3(46, 0x1A),// + CONTEXT4(47, 0x1B),// + PAD1_A(36, 0x1C, true),// + PAD1_B(44, 0x24, true),// + ; + + private final int ccId; + private final int itemId; + private final boolean isMultiBase; + + CCAssignment(final int ccId, final int itemId, final boolean isMultiBase) { + this.ccId = ccId; + this.itemId = itemId; + this.isMultiBase = isMultiBase; + } + + CCAssignment(final int ccId, final int itemId) { + this(ccId, itemId, false); + } + + public int getItemId() { + return itemId; + } + + public int getCcId() { + return ccId; + } + + public boolean isMultiBase() { + return isMultiBase; + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/ClipLaunchingLayer.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/ClipLaunchingLayer.java index 482ece14..3e1cc26e 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/ClipLaunchingLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/ClipLaunchingLayer.java @@ -1,5 +1,7 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; +import java.util.Arrays; + import com.bitwig.extension.controller.api.ClipLauncherSlot; import com.bitwig.extension.controller.api.Track; import com.bitwig.extension.controller.api.TrackBank; @@ -11,180 +13,165 @@ import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.ViewControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; -import com.bitwig.extensions.framework.time.TimedEvent; - -import java.util.Arrays; public class ClipLaunchingLayer extends Layer { - - private final RgbColor[] sceneSlotColors = new RgbColor[8]; - private final long[] downTime = new long[4]; - private final TimedEvent[] holdEvent = new TimedEvent[4]; - private final Layer sceneLaunchingLayer; // clips in selected scene - private final TrackBank trackBank; - private long clipsStopTiming = 800; - - public ClipLaunchingLayer(final Layers layers, final ViewControl viewControl, final HwElements hwElements, - SysExHandler sysExHandler) { - super(layers, "CLIP LAUNCHER"); - - sceneLaunchingLayer = new Layer(layers, "PER_SCENE_LAUNCHER"); - sysExHandler.registerTickTask(this::notifyTick); - Arrays.fill(sceneSlotColors, RgbColor.OFF); - Arrays.fill(downTime, -1); - - final RgbButton[] buttons = hwElements.getPadBankAButtons(); - - trackBank = viewControl.getViewTrackBank(); - trackBank.setShouldShowClipLauncherFeedback(true); - -// final RgbBankLightState.Handler bankLightHandler = new RgbBankLightState.Handler( -// PadBank.BANK_A, buttons); - - setUpLaunching(buttons); -// driver.getPadBank().addValueObserver(((oldValue, newValue) -> changePadBank(newValue))); - } - - public void setClipStopTiming(final String timingValue) { - switch (timingValue) { - case "Fast": - clipsStopTiming = 500; - break; - case "Medium": - clipsStopTiming = 800; - break; - case "Standard": - clipsStopTiming = 1000; - break; - } - } - - public void notifyTick() { - final long time = System.currentTimeMillis(); - for (int i = 0; i < 4; i++) { - if (downTime[i] != -1 && (time - downTime[i]) > clipsStopTiming) { - final Track track = trackBank.getItemAt(i); - track.stop(); - } - } - } - - private void changePadBank(final PadBank newValue) { - if (!isActive()) { - return; - } - activateIndication(newValue == PadBank.BANK_A); - } - - void activateIndication(final boolean indication) { - for (int i = 0; i < 8; i++) { - trackBank.sceneBank().setIndication(indication); - trackBank.setShouldShowClipLauncherFeedback(indication); - } - } - - private void setUpLaunching(final RgbButton[] buttons) { - for (int i = 0; i < 8; i++) { - final int buttonIndex = i; - final int trackIndex = i % 4; - final int sceneIndex = i / 4; - final Track track = trackBank.getItemAt(trackIndex); - - final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); - prepareSlot(slot); - track.isQueuedForStop().markInterested(); - track.arm().markInterested(); - slot.color().addValueObserver((r, g, b) -> sceneSlotColors[buttonIndex] = RgbColor.getColor(r, g, b)); - final RgbButton button = buttons[i]; - button.bindIsPressed(sceneLaunchingLayer, pressed -> handleSlotSelected(buttonIndex, track, slot, pressed)); - button.bindLight(sceneLaunchingLayer, () -> getLightState(buttonIndex, track, slot)); - } - } - - public void navigateScenes(final int direction) { - trackBank.sceneBank().scrollBy(direction); - } - - public void launchScene() { - trackBank.sceneBank().getScene(0).launch(); - } - - private void prepareSlot(final ClipLauncherSlot cs) { - cs.exists().markInterested(); - cs.hasContent().markInterested(); - cs.isPlaybackQueued().markInterested(); - cs.isPlaying().markInterested(); - cs.isRecording().markInterested(); - cs.isRecordingQueued().markInterested(); - cs.isSelected().markInterested(); - cs.isStopQueued().markInterested(); - } - - private void handleSlotSelected(final int buttonIndex, final Track track, final ClipLauncherSlot slot, - final boolean pressed) { - final int trackIndex = buttonIndex % 4; - if (pressed) { - downTime[trackIndex] = System.currentTimeMillis(); - slot.select(); - if (slot.isRecording().get()) { - slot.launch(); - } - } else { - final long diff = System.currentTimeMillis() - downTime[trackIndex]; - if (!slot.isRecording().get()) { - if (diff > clipsStopTiming) { - track.stop(); - } else { - slot.launch(); + + private final RgbColor[] sceneSlotColors = new RgbColor[8]; + private final long[] downTime = new long[4]; + private final Layer sceneLaunchingLayer; // clips in selected scene + private final TrackBank trackBank; + private long clipsStopTiming = 800; + + public ClipLaunchingLayer(final Layers layers, final ViewControl viewControl, final HwElements hwElements, + final SysExHandler sysExHandler) { + super(layers, "CLIP LAUNCHER"); + + sceneLaunchingLayer = new Layer(layers, "PER_SCENE_LAUNCHER"); + sysExHandler.registerTickTask(this::notifyTick); + Arrays.fill(sceneSlotColors, RgbColor.OFF); + Arrays.fill(downTime, -1); + + final RgbButton[] buttons = hwElements.getPadBankAButtons(); + + trackBank = viewControl.getViewTrackBank(); + trackBank.setShouldShowClipLauncherFeedback(true); + + setUpLaunching(buttons); + } + + public void setClipStopTiming(final String timingValue) { + switch (timingValue) { + case "Fast": + clipsStopTiming = 500; + break; + case "Medium": + clipsStopTiming = 800; + break; + case "Standard": + clipsStopTiming = 1000; + break; + } + } + + public void notifyTick() { + final long time = System.currentTimeMillis(); + for (int i = 0; i < 4; i++) { + if (downTime[i] != -1 && (time - downTime[i]) > clipsStopTiming) { + final Track track = trackBank.getItemAt(i); + track.stop(); } - } - downTime[trackIndex] = -1; - } - } - - RgbLightState getLightState(final int index, final Track track, final ClipLauncherSlot slot) { - final RgbColor color = sceneSlotColors[index]; - if (slot.hasContent().get()) { - if (slot.isRecordingQueued().get()) { - return RgbColor.RED.getColorState(BlinkState.BLINK2); // RgbState.flash(color, 5); - } else if (slot.isRecording().get()) { - return RgbColor.RED.getColorState(BlinkState.BLINK3); // RgbState.pulse(5); - } else if (slot.isPlaybackQueued().get()) { - return color.getColorState(BlinkState.BLINK3); // RgbState.flash(color, 0); - } else if (slot.isStopQueued().get()) { - return color.getColorState(BlinkState.BLINK2); // RgbState.flash(color, 1); - } else if (slot.isPlaying().get() && track.isQueuedForStop().get()) { - return color.getColorState(BlinkState.BLINK2); //RgbState.flash(color, 1); - } else if (slot.isPlaying().get()) { - return RgbColor.GREEN.getColorState(); -// if (clipLauncherOverdub.get() && track.arm().get()) { -// return RgbState.pulse(5); -// } else { -// return RgbState.pulse(22); -// } - } - return color.getColorState(); - } - if (slot.isRecordingQueued().get()) { - return RgbColor.RED.getColorState(BlinkState.BLINK2); - } else if (track.arm().get()) { - return RgbColor.RED.getColorState(); - } - return RgbLightState.OFF; - } - - @Override - protected void onActivate() { - super.onActivate(); - sceneLaunchingLayer.activate(); - //activateIndication(driver.getPadBank().get() == PadBank.BANK_A); - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - sceneLaunchingLayer.deactivate(); - activateIndication(false); - } - + } + } + + void activateIndication(final boolean indication) { + for (int i = 0; i < 8; i++) { + trackBank.sceneBank().setIndication(indication); + trackBank.setShouldShowClipLauncherFeedback(indication); + } + } + + private void setUpLaunching(final RgbButton[] buttons) { + for (int i = 0; i < 8; i++) { + final int buttonIndex = i; + final int trackIndex = i % 4; + final int sceneIndex = i / 4; + final Track track = trackBank.getItemAt(trackIndex); + + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + prepareSlot(slot); + track.isQueuedForStop().markInterested(); + track.arm().markInterested(); + slot.color().addValueObserver((r, g, b) -> sceneSlotColors[buttonIndex] = RgbColor.getColor(r, g, b)); + final RgbButton button = buttons[i]; + button.bindIsPressed(sceneLaunchingLayer, pressed -> handleSlotSelected(buttonIndex, track, slot, pressed)); + button.bindLight(sceneLaunchingLayer, () -> getLightState(buttonIndex, track, slot)); + } + } + + public void navigateScenes(final int direction) { + trackBank.sceneBank().scrollBy(direction); + } + + public void launchScene() { + trackBank.sceneBank().getScene(0).launch(); + } + + private void prepareSlot(final ClipLauncherSlot cs) { + cs.exists().markInterested(); + cs.hasContent().markInterested(); + cs.isPlaybackQueued().markInterested(); + cs.isPlaying().markInterested(); + cs.isRecording().markInterested(); + cs.isRecordingQueued().markInterested(); + cs.isSelected().markInterested(); + cs.isStopQueued().markInterested(); + } + + private void handleSlotSelected(final int buttonIndex, final Track track, final ClipLauncherSlot slot, + final boolean pressed) { + final int trackIndex = buttonIndex % 4; + if (pressed) { + downTime[trackIndex] = System.currentTimeMillis(); + slot.select(); + if (slot.isRecording().get()) { + slot.launch(); + } + } else { + final long diff = System.currentTimeMillis() - downTime[trackIndex]; + if (!slot.isRecording().get()) { + if (diff > clipsStopTiming) { + track.stop(); + } else { + slot.launch(); + } + } + downTime[trackIndex] = -1; + } + } + + RgbLightState getLightState(final int index, final Track track, final ClipLauncherSlot slot) { + final RgbColor color = sceneSlotColors[index]; + if (slot.hasContent().get()) { + if (slot.isRecordingQueued().get()) { + return RgbColor.RED.getColorState(BlinkState.BLINK2); // RgbState.flash(color, 5); + } else if (slot.isRecording().get()) { + return RgbColor.RED.getColorState(BlinkState.BLINK3); // RgbState.pulse(5); + } else if (slot.isPlaybackQueued().get()) { + return color.getColorState(BlinkState.BLINK3); // RgbState.flash(color, 0); + } else if (slot.isStopQueued().get()) { + return color.getColorState(BlinkState.BLINK2); // RgbState.flash(color, 1); + } else if (slot.isPlaying().get() && track.isQueuedForStop().get()) { + return color.getColorState(BlinkState.BLINK2); //RgbState.flash(color, 1); + } else if (slot.isPlaying().get()) { + return RgbColor.GREEN.getColorState(); + // if (clipLauncherOverdub.get() && track.arm().get()) { + // return RgbState.pulse(5); + // } else { + // return RgbState.pulse(22); + // } + } + return color.getColorState(); + } + if (slot.isRecordingQueued().get()) { + return RgbColor.RED.getColorState(BlinkState.BLINK2); + } else if (track.arm().get()) { + return RgbColor.RED.getColorState(); + } + return RgbLightState.OFF; + } + + @Override + protected void onActivate() { + super.onActivate(); + sceneLaunchingLayer.activate(); + //activateIndication(driver.getPadBank().get() == PadBank.BANK_A); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + sceneLaunchingLayer.deactivate(); + activateIndication(false); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/DrumPadLayer.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/DrumPadLayer.java index dde65b5b..135264ec 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/DrumPadLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/DrumPadLayer.java @@ -1,6 +1,16 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; -import com.bitwig.extension.controller.api.*; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.DrumPad; +import com.bitwig.extension.controller.api.DrumPadBank; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.PlayingNote; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.color.RgbColor; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.color.RgbLightState; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.HwElements; @@ -9,189 +19,168 @@ import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.PostConstruct; import com.bitwig.extensions.framework.values.BasicStringValue; -import com.bitwig.extensions.framework.values.Midi; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; public class DrumPadLayer extends Layer { - private static final String[] noteValues = {"C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B "}; - - private final RgbColor[] colorSlots = new RgbColor[8]; - //protected final Integer[] velTable = new Integer[128]; - private final Integer[] noteTable = new Integer[128]; - private final boolean[] isPlaying = new boolean[128]; - private final Set padNotes = new HashSet<>(); - private final NoteInput noteInput; - private DrumPadBank padBank; - private RgbColor trackColor = RgbColor.OFF; - private boolean focusOnDrums = false; - private int keysNoteOffset = 60; - private int padsNoteOffset = 32; - private final int[] hangingNotes = new int[8]; - private final BasicStringValue padLocationInfo = new BasicStringValue(""); - - public DrumPadLayer(final Layers layers, MidiIn midiIn) { - super(layers, "DRUM_PAD"); - noteInput = midiIn.createNoteInput("MIDI", "8A????", "9A????", "AA????"); - noteInput.setShouldConsumeEvents(false); - } - - @PostConstruct - public void init(ViewControl viewControl, HwElements hwElements) { - final PinnableCursorDevice primaryDevice = viewControl.getPrimaryDevice(); - primaryDevice.hasDrumPads().addValueObserver(this::handleHasDrumsChanged); - padBank = primaryDevice.createDrumPadBank(8); - padBank.scrollPosition().addValueObserver(position -> { - padsNoteOffset = position; - if (isActive()) { - applyNotes(padsNoteOffset); - } - updateInfo(); - }); - padBank.scrollPosition().set(30); - padBank.setIndication(true); - primaryDevice.hasDrumPads().addValueObserver(hasDrumPads -> { - if (isActive()) { - applyNotes(hasDrumPads ? padsNoteOffset : keysNoteOffset); - } - updateInfo(); - }); - padsNoteOffset = padBank.scrollPosition().get(); - Arrays.fill(noteTable, -1); - Arrays.fill(colorSlots, RgbColor.GREEN); - Arrays.fill(hangingNotes, -1); - - final CursorTrack cursorTrack = viewControl.getCursorTrack(); - - final RgbButton[] buttons = hwElements.getPadBankBButtons(); - for (int i = 0; i < buttons.length; i++) { - final int index = i; - final int buttonIndex = i % 4 + (1 - (i / 4)) * 4; - final RgbButton button = buttons[buttonIndex]; - - padNotes.add(button.getNoteValue()); - final DrumPad pad = padBank.getItemAt(i); - pad.exists().markInterested(); - - pad.color().addValueObserver((r, g, b) -> colorSlots[index] = RgbColor.getColor(r, g, b)); - button.bindLight(this, () -> getLightState(index, pad)); - } - - cursorTrack.playingNotes().addValueObserver(this::handleNotes); - cursorTrack.color().addValueObserver((r, g, b) -> trackColor = RgbColor.getColor(r, g, b)); - } - - public void notifyNote(final int sb, final int noteValue) { - if (!isActive() || !padNotes.contains(noteValue) || (sb != Midi.NOTE_ON && sb != Midi.NOTE_OFF)) { - return; - } - final int index = noteValue - 44; - final int notePlayed = focusOnDrums ? padsNoteOffset + index : keysNoteOffset + index; - if (sb == Midi.NOTE_ON) { - hangingNotes[index] = notePlayed; - } else { - if (hangingNotes[index] != -1 && hangingNotes[index] != notePlayed) { - noteInput.sendRawMidiEvent(Midi.NOTE_OFF | 9, hangingNotes[index], 0); - } - hangingNotes[index] = -1; - } - } - - private void handleHasDrumsChanged(final boolean hasDrums) { - focusOnDrums = hasDrums; - } - - public BasicStringValue getPadLocationInfo() { - return padLocationInfo; - } - - private void handleNotes(final PlayingNote[] playingNotes) { - if (!isActive()) { - return; - } - Arrays.fill(isPlaying, false); - for (final PlayingNote playingNote : playingNotes) { - isPlaying[playingNote.pitch()] = true; - } - } - - public void navigate(final int dir) { - if (focusOnDrums) { - //driver.getOled().enableValues(DisplayMode.SCENE); - padBank.scrollBy(dir * 4); - } else { - final int newOffset = keysNoteOffset + dir * 4; - if (newOffset >= 0 && newOffset <= 116) { - //muteNoteFromOffset(keysNoteOffset); - keysNoteOffset = newOffset; - applyNotes(keysNoteOffset); + private static final String[] noteValues = {"C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B "}; + + private final RgbColor[] colorSlots = new RgbColor[8]; + //protected final Integer[] velTable = new Integer[128]; + private final Integer[] noteTable = new Integer[128]; + private final boolean[] isPlaying = new boolean[128]; + private final Set padNotes = new HashSet<>(); + private final NoteInput noteInput; + private DrumPadBank padBank; + private RgbColor trackColor = RgbColor.OFF; + private boolean focusOnDrums = false; + private int keysNoteOffset = 60; + private int padsNoteOffset = 32; + private final int[] hangingNotes = new int[8]; + private final BasicStringValue padLocationInfo = new BasicStringValue(""); + + public DrumPadLayer(final Layers layers, final MidiIn midiIn) { + super(layers, "DRUM_PAD"); + noteInput = midiIn.createNoteInput("MIDI", "8A????", "9A????", "AA????"); + noteInput.setShouldConsumeEvents(false); + } + + @PostConstruct + public void init(final ViewControl viewControl, final HwElements hwElements) { + final PinnableCursorDevice primaryDevice = viewControl.getPrimaryDevice(); + primaryDevice.hasDrumPads().addValueObserver(this::handleHasDrumsChanged); + padBank = primaryDevice.createDrumPadBank(8); + padBank.scrollPosition().addValueObserver(position -> { + padsNoteOffset = position; + if (isActive()) { + applyNotes(padsNoteOffset); + } updateInfo(); - } - } - } - - private void updateInfo() { - if (focusOnDrums) { - padLocationInfo.set( - String.format(String.format("Pads %s - %s", toNote(padsNoteOffset), toNote(padsNoteOffset + 7)))); - } else { - padLocationInfo.set( - String.format(String.format("Pads %s - %s", toNote(keysNoteOffset), toNote(keysNoteOffset + 7)))); - } - } - - private String toNote(final int midiValue) { - final int noteValue = midiValue % 12; - final int octave = (midiValue / 12) - 2; - return noteValues[noteValue] + octave; - } - - private RgbLightState getLightState(final int index, final DrumPad pad) { - final int noteValue = (focusOnDrums ? padsNoteOffset : keysNoteOffset) + index; - if (noteValue < 128) { - final boolean notePlaying = isPlaying[noteValue]; - if (focusOnDrums) { - if (pad.exists().get()) { - if (colorSlots[index] != null) { - return notePlaying ? RgbLightState.WHITE : colorSlots[index].getColorState(); - } - } else { - return RgbLightState.OFF; + }); + padBank.scrollPosition().set(30); + padBank.setIndication(true); + primaryDevice.hasDrumPads().addValueObserver(hasDrumPads -> { + if (isActive()) { + applyNotes(hasDrumPads ? padsNoteOffset : keysNoteOffset); + } + updateInfo(); + }); + padsNoteOffset = padBank.scrollPosition().get(); + Arrays.fill(noteTable, -1); + Arrays.fill(colorSlots, RgbColor.GREEN); + Arrays.fill(hangingNotes, -1); + + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + + final RgbButton[] buttons = hwElements.getPadBankBButtons(); + for (int i = 0; i < buttons.length; i++) { + final int index = i; + final int buttonIndex = i % 4 + (1 - (i / 4)) * 4; + final RgbButton button = buttons[buttonIndex]; + + padNotes.add(button.getNoteValue()); + final DrumPad pad = padBank.getItemAt(i); + pad.exists().markInterested(); + + pad.color().addValueObserver((r, g, b) -> colorSlots[index] = RgbColor.getColor(r, g, b)); + button.bindLight(this, () -> getLightState(index, pad)); + } + + cursorTrack.playingNotes().addValueObserver(this::handleNotes); + cursorTrack.color().addValueObserver((r, g, b) -> trackColor = RgbColor.getColor(r, g, b)); + } + + private void handleHasDrumsChanged(final boolean hasDrums) { + focusOnDrums = hasDrums; + } + + public BasicStringValue getPadLocationInfo() { + return padLocationInfo; + } + + private void handleNotes(final PlayingNote[] playingNotes) { + if (!isActive()) { + return; + } + Arrays.fill(isPlaying, false); + for (final PlayingNote playingNote : playingNotes) { + isPlaying[playingNote.pitch()] = true; + } + } + + public void navigate(final int dir) { + if (focusOnDrums) { + //driver.getOled().enableValues(DisplayMode.SCENE); + padBank.scrollBy(dir * 4); + } else { + final int newOffset = keysNoteOffset + dir * 4; + if (newOffset >= 0 && newOffset <= 116) { + //muteNoteFromOffset(keysNoteOffset); + keysNoteOffset = newOffset; + applyNotes(keysNoteOffset); + updateInfo(); + } + } + } + + private void updateInfo() { + if (focusOnDrums) { + padLocationInfo.set( + String.format(String.format("Pads %s - %s", toNote(padsNoteOffset), toNote(padsNoteOffset + 7)))); + } else { + padLocationInfo.set( + String.format(String.format("Pads %s - %s", toNote(keysNoteOffset), toNote(keysNoteOffset + 7)))); + } + } + + private String toNote(final int midiValue) { + final int noteValue = midiValue % 12; + final int octave = (midiValue / 12) - 2; + return noteValues[noteValue] + octave; + } + + private RgbLightState getLightState(final int index, final DrumPad pad) { + final int noteValue = (focusOnDrums ? padsNoteOffset : keysNoteOffset) + index; + if (noteValue < 128) { + final boolean notePlaying = isPlaying[noteValue]; + if (focusOnDrums) { + if (pad.exists().get()) { + if (colorSlots[index] != null) { + return notePlaying ? RgbLightState.WHITE : colorSlots[index].getColorState(); + } + } else { + return RgbLightState.OFF; + } + return notePlaying ? RgbLightState.WHITE : trackColor.getColorState(); } return notePlaying ? RgbLightState.WHITE : trackColor.getColorState(); - } - return notePlaying ? RgbLightState.WHITE : trackColor.getColorState(); - } else { - return RgbLightState.OFF; - } - } - - public void applyNotes(final int noteOffset) { - Arrays.fill(noteTable, -1); - for (int note = 0; note < 8; note++) { - final int value = noteOffset + note; - noteTable[0x2c + note] = value < 128 ? value : -1; - } - noteInput.setKeyTranslationTable(noteTable); - } - - private void resetNotes() { - Arrays.fill(noteTable, -1); - noteInput.setKeyTranslationTable(noteTable); - } - - @Override - protected void onActivate() { - super.onActivate(); - applyNotes(focusOnDrums ? padsNoteOffset : keysNoteOffset); - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - resetNotes(); - } - + } else { + return RgbLightState.OFF; + } + } + + public void applyNotes(final int noteOffset) { + Arrays.fill(noteTable, -1); + for (int note = 0; note < 8; note++) { + final int value = noteOffset + note; + noteTable[0x2c + note] = value < 128 ? value : -1; + } + noteInput.setKeyTranslationTable(noteTable); + } + + private void resetNotes() { + Arrays.fill(noteTable, -1); + noteInput.setKeyTranslationTable(noteTable); + } + + @Override + protected void onActivate() { + super.onActivate(); + applyNotes(focusOnDrums ? padsNoteOffset : keysNoteOffset); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + resetNotes(); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/KeyLabEssential3ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/KeyLabEssential3ExtensionDefinition.java index 60a6c8d4..7af44675 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/KeyLabEssential3ExtensionDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/KeyLabEssential3ExtensionDefinition.java @@ -1,103 +1,104 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; +import java.util.UUID; + import com.bitwig.extension.api.PlatformType; import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; import com.bitwig.extension.controller.ControllerExtensionDefinition; import com.bitwig.extension.controller.api.ControllerHost; -import java.util.UUID; - public class KeyLabEssential3ExtensionDefinition extends ControllerExtensionDefinition { - private static final UUID DRIVER_ID = UUID.fromString("0b9962d9-5c32-4d5a-942c-594dbe0c64ca"); - - private static final String MIDI_NAME_FORMAT_WINDOWS = "KL Essential %d mk3 MIDI"; - private static final String MIDI_NAME_FORMAT_MAC = "KeyLab Essential %d mk3 MIDI"; - - private static final int[] KEY_VARS = {49, 61, 88}; - - public KeyLabEssential3ExtensionDefinition() { - } - - @Override - public String getName() { - return "KeyLab Essential Mk3"; - } - - @Override - public String getAuthor() { - return "Bitwig"; - } - - @Override - public String getVersion() { - return "1.01"; - } - - @Override - public UUID getId() { - return DRIVER_ID; - } - - @Override - public String getHardwareVendor() { - return "Arturia"; - } - - @Override - public String getHardwareModel() { - return "KeyLab Essential Mk3"; - } - - @Override - public int getRequiredAPIVersion() { - return 17; - } - - @Override - public int getNumMidiInPorts() { - return 1; - } - - @Override - public int getNumMidiOutPorts() { - return 1; - } - - @Override - public String getHelpFilePath() { - return "Controllers/Arturia/Arturia KeyLab Essential Mk3.pdf"; - } - - @Override - public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, - final PlatformType platformType) { - if (platformType == PlatformType.WINDOWS) { - addPorts(list, MIDI_NAME_FORMAT_WINDOWS, 4); - } else if (platformType == PlatformType.MAC) { - addPorts(list, MIDI_NAME_FORMAT_MAC, 0); - } else if (platformType == PlatformType.LINUX) { - addPorts(list, MIDI_NAME_FORMAT_MAC, 0); - } - } - - private void addPorts(final AutoDetectionMidiPortNamesList list, final String format, int appendWindowsCount) { - for (final int keyNumber : KEY_VARS) { - addPorts(list, format, keyNumber, appendWindowsCount); - } - } - - private void addPorts(final AutoDetectionMidiPortNamesList list, final String format, final int numberOfKeys, - int appendWindowsCount) { - final String portName = String.format(format, numberOfKeys); - list.add(new String[]{portName}, new String[]{portName}); - for (int i = 0; i < appendWindowsCount; i++) { - String portAppended = "%d- %s".formatted(i + 2, portName); - list.add(new String[]{portAppended}, new String[]{portAppended}); - } - } - - @Override - public KeylabEssential3Extension createInstance(final ControllerHost host) { - return new KeylabEssential3Extension(this, host); - } + private static final UUID DRIVER_ID = UUID.fromString("0b9962d9-5c32-4d5a-942c-594dbe0c64ca"); + + private static final String MIDI_NAME_FORMAT_WINDOWS = "KL Essential %d mk3 MIDI"; + private static final String MIDI_NAME_FORMAT_MAC = "KeyLab Essential %d mk3 MIDI"; + + private static final int[] KEY_VARS = {49, 61, 88}; + + public KeyLabEssential3ExtensionDefinition() { + } + + @Override + public String getName() { + return "KeyLab Essential Mk3"; + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "1.1"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareVendor() { + return "Arturia"; + } + + @Override + public String getHardwareModel() { + return "KeyLab Essential Mk3"; + } + + @Override + public int getRequiredAPIVersion() { + return 17; + } + + @Override + public int getNumMidiInPorts() { + return 1; + } + + @Override + public int getNumMidiOutPorts() { + return 1; + } + + @Override + public String getHelpFilePath() { + return "Controllers/Arturia/Arturia KeyLab Essential Mk3.pdf"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { + addPorts(list, MIDI_NAME_FORMAT_WINDOWS, 4); + } else if (platformType == PlatformType.MAC) { + addPorts(list, MIDI_NAME_FORMAT_MAC, 0); + } else if (platformType == PlatformType.LINUX) { + addPorts(list, MIDI_NAME_FORMAT_MAC, 0); + } + } + + private void addPorts(final AutoDetectionMidiPortNamesList list, final String format, + final int appendWindowsCount) { + for (final int keyNumber : KEY_VARS) { + addPorts(list, format, keyNumber, appendWindowsCount); + } + } + + private void addPorts(final AutoDetectionMidiPortNamesList list, final String format, final int numberOfKeys, + final int appendWindowsCount) { + final String portName = String.format(format, numberOfKeys); + list.add(new String[] {portName}, new String[] {portName}); + for (int i = 0; i < appendWindowsCount; i++) { + final String portAppended = "%d- %s".formatted(i + 2, portName); + list.add(new String[] {portAppended}, new String[] {portAppended}); + } + } + + @Override + public KeylabEssential3Extension createInstance(final ControllerHost host) { + return new KeylabEssential3Extension(this, host); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/KeylabEssential3Extension.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/KeylabEssential3Extension.java index a264c966..27c3f465 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/KeylabEssential3Extension.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/KeylabEssential3Extension.java @@ -1,8 +1,26 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; -import com.bitwig.extension.api.util.midi.ShortMidiMessage; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; + import com.bitwig.extension.controller.ControllerExtension; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.Action; +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.Clip; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extension.controller.api.Preferences; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extension.controller.api.Transport; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.color.RgbLightState; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.HwElements; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.SysExHandler; @@ -13,307 +31,328 @@ import com.bitwig.extensions.framework.di.Context; import com.bitwig.extensions.framework.time.TimedDelayEvent; import com.bitwig.extensions.framework.values.FocusMode; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; +import com.bitwig.extensions.framework.values.LayoutType; public class KeylabEssential3Extension extends ControllerExtension { - - private Layer mainLayer; - - private HardwareSurface surface; - private ControllerHost host; - private Transport transport; - private SysExHandler sysExHandler; - - private Runnable nextPingAction = null; - private FocusMode recordFocusMode = FocusMode.ARRANGER; - private ClipLaunchingLayer clipLaunchingLayer; - private LcdDisplay lcdDisplay; - private MainScreenSection mainScreenSection; - private DrumPadLayer drumPadLayer; - private SysExHandler.PadMode padMode = SysExHandler.PadMode.PAD_CLIPS; - private boolean buttonPadRequestInvoked = false; - - private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); - private static ControllerHost debugHost; - - public static void println(final String format, final Object... args) { - if (debugHost != null) { - final LocalDateTime now = LocalDateTime.now(); - debugHost.println(now.format(DF) + " > " + String.format(format, args)); - } - } - - protected KeylabEssential3Extension(final KeyLabEssential3ExtensionDefinition definition, - final ControllerHost host) { - super(definition, host); - } - - @Override - public void init() { - host = getHost(); - debugHost = host; - final Context diContext = new Context(this); - surface = diContext.getService(HardwareSurface.class); - MidiIn midiIn = host.getMidiInPort(0); - //midiIn.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi0); - MidiOut midiOut = host.getMidiOutPort(0); - diContext.registerService(MidiIn.class, midiIn); - diContext.registerService(MidiOut.class, midiOut); - sysExHandler = diContext.getService(SysExHandler.class); - lcdDisplay = diContext.getService(LcdDisplay.class); - sysExHandler.addSysExEventListener(this::handleSysExEvent); - - NoteInput noteInput = midiIn.createNoteInput("MIDI", - getInputMask(0x0A, new int[]{0x1, 0x40, 0x72, 0x73, 0x1E, 0x1F, 0x56, 0x57, 0x49, 0x4B, 0x4F, 0x48, // - 0x50, 0x51, 0x52, 0x53, 0x55, 0x5A, 0x4A, 0x47, 0x4C, 0x4D, 0x5D, 0x12, 0x13, 0x10, 0x11})); - noteInput.setShouldConsumeEvents(true); - - mainLayer = diContext.createLayer("MAIN"); - clipLaunchingLayer = diContext.create(ClipLaunchingLayer.class); - drumPadLayer = diContext.create(DrumPadLayer.class); - diContext.create(SliderEncoderControl.class); - mainScreenSection = diContext.getService(MainScreenSection.class); - diContext.create(BrowserLayer.class); - - sysExHandler.addPadModeEventListener(this::handlePadModeChanged); - - setUpTransportControl(diContext); - initEncoders(diContext); - initPadBankHandling(diContext); - - sysExHandler.deviceInquiry(); - - mainScreenSection.setPadInfo(drumPadLayer.getPadLocationInfo()); - mainLayer.activate(); - clipLaunchingLayer.activate(); - drumPadLayer.activate(); - - setUpPreferences(); - diContext.activate(); - host.scheduleTask(this::handlePing, 100); - } - - private SysExHandler.SysexEventType mode = SysExHandler.SysexEventType.DAW_MODE; - - private void handleSysExEvent(final SysExHandler.SysexEventType sysexEventType) { - switch (sysexEventType) { - case DAW_MODE: - println(" Into Daw Mode"); - clipLaunchingLayer.activateIndication(true); - mode = SysExHandler.SysexEventType.DAW_MODE; - break; - case ARTURIA_MODE: - println(" Into Arturia Mode"); - mode = SysExHandler.SysexEventType.ARTURIA_MODE; - break; - case USER_MODE: - println(" USER MODE"); - mode = SysExHandler.SysexEventType.USER_MODE; - break; - case INIT: - lcdDisplay.logoText("Bitwig", "connected", KeylabIcon.BITWIG); - sysExHandler.requestPadBank(); - sysExHandler.queueTimedEvent(new TimedDelayEvent(() -> mainScreenSection.updatePage(), 2000)); - break; - } - } - - private void handlePing() { - lcdDisplay.ping(); - if (nextPingAction != null) { - nextPingAction.run(); - nextPingAction = null; - } - host.scheduleTask(this::handlePing, 100); - } - - private String[] getInputMask(final int excludeChannel, final int[] miniLabPassThroughCcs) { - final List masks = new ArrayList<>(); - for (int i = 0; i < 16; i++) { - if (i != excludeChannel) { - masks.add(String.format("8%01x????", i)); - masks.add(String.format("9%01x????", i)); - } - } - masks.add("A?????"); // Poly Aftertouch - masks.add("D?????"); // Channel Aftertouch - masks.add("E?????"); // Pitchbend - masks.add("B1????"); // CCs Channel 2 - //masks.add("B0????"); - for (final int miniLabPassThroughCc : miniLabPassThroughCcs) { - masks.add(String.format("B0%02x??", miniLabPassThroughCc)); - } - return masks.toArray(String[]::new); - } - - private void setUpPreferences() { - final Preferences preferences = getHost().getPreferences(); // THIS - final SettableEnumValue recordButtonAssignment = preferences.getEnumSetting("Record Button assignment", // - "Transport", new String[]{FocusMode.LAUNCHER.getDescriptor(), FocusMode.ARRANGER.getDescriptor()}, - recordFocusMode.getDescriptor()); - recordButtonAssignment.addValueObserver(value -> recordFocusMode = FocusMode.toMode(value)); - final SettableEnumValue clipStopTiming = preferences.getEnumSetting("Long press to stop clip", // - "Clip", new String[]{"Fast", "Medium", "Standard"}, "Medium"); - clipStopTiming.addValueObserver(clipLaunchingLayer::setClipStopTiming); - } - - private void initEncoders(final Context diContext) { - final HwElements hwElements = diContext.getService(HwElements.class); - hwElements.bindEncoder(mainLayer, hwElements.getMainEncoder(), this::mainEncoderAction); - mainLayer.bindPressed(hwElements.getEncoderPress(), this::handleEncoderPressed); - } - - private void handlePadModeChanged(SysExHandler.PadMode padMode) { - this.padMode = buttonPadRequestInvoked ? padMode.inverted() : padMode; - mainScreenSection.notifyPadMode(this.padMode); - buttonPadRequestInvoked = false; - } - - private void initPadBankHandling(Context diContext) { - final HwElements hwElements = diContext.getService(HwElements.class); - HardwareButton bankButton = hwElements.getBankButton(); - mainLayer.bindPressed(bankButton, () -> { - sysExHandler.requestPadBank(); - buttonPadRequestInvoked = true; - }); - } - - private void handleEncoderPressed() { - if (padMode == SysExHandler.PadMode.PAD_CLIPS) { - clipLaunchingLayer.launchScene(); - } - } - - private void setUpTransportControl(final Context diContext) { - final HwElements hwElements = diContext.getService(HwElements.class); - transport = diContext.getService(Transport.class); - final Application application = diContext.getService(Application.class); - final ViewControl viewControl = diContext.getService(ViewControl.class); - transport.isArrangerRecordEnabled().markInterested(); - transport.isClipLauncherOverdubEnabled().markInterested(); - - final RgbButton loopButton = hwElements.getButton(CCAssignment.LOOP); - loopButton.bindToggle(mainLayer, transport.isArrangerLoopEnabled(), RgbLightState.ORANGE, - RgbLightState.ORANGE_DIMMED); - final RgbButton playButton = hwElements.getButton(CCAssignment.PLAY); - transport.isPlaying().markInterested(); - playButton.bindPressed(mainLayer, this::handlePlayPressed); - playButton.bindLight(mainLayer, - () -> transport.isPlaying().get() ? RgbLightState.GREEN : RgbLightState.GREEN_DIMMED); - - final RgbButton stopButton = hwElements.getButton(CCAssignment.STOP); - stopButton.bindPressed(mainLayer, () -> transport.stop()); - stopButton.bindLight(mainLayer, - () -> transport.isPlaying().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); - - final RgbButton recordButton = hwElements.getButton(CCAssignment.REC); - recordButton.bindPressed(mainLayer, this::handleRecordPressed); - recordButton.bindLight(mainLayer, this::getRecordingLightState); - - final RgbButton tapButton = hwElements.getButton(CCAssignment.TAP); - tapButton.bind(mainLayer, transport.tapTempoAction(), RgbLightState.WHITE, RgbLightState.WHITE_DIMMED); - final RgbButton metroButton = hwElements.getButton(CCAssignment.METRO); - metroButton.bindToggle(mainLayer, transport.isMetronomeEnabled(), RgbLightState.WHITE, - RgbLightState.WHITE_DIMMED); - - final RgbButton fastForwardButton = hwElements.getButton(CCAssignment.FFWD); - final RgbButton rewindButton = hwElements.getButton(CCAssignment.RWD); - fastForwardButton.bindRepeatHold(mainLayer, () -> transport.fastForward(), 400, 100); - fastForwardButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); - - rewindButton.bindRepeatHold(mainLayer, () -> transport.rewind(), 400, 100); - rewindButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); - - application.canUndo().markInterested(); - application.canRedo().markInterested(); - final RgbButton undoButton = hwElements.getButton(CCAssignment.UNDO); - undoButton.bindPressed(mainLayer, application::undo); - undoButton.bindLight(mainLayer, - () -> application.canUndo().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); - - final RgbButton redoButton = hwElements.getButton(CCAssignment.REDO); - redoButton.bindPressed(mainLayer, application::redo); - redoButton.bindLight(mainLayer, - () -> application.canRedo().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); - - // TODO Punch Button does nothing - final RgbButton punchButton = hwElements.getButton(CCAssignment.PUNCH); - punchButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); - punchButton.bindPressed(mainLayer, viewControl::invokeQuantize); - // punchButton.bind(mainLayer, this::customAction, RgbLightState.WHITE, RgbLightState.WHITE_DIMMED); - final Action saveAction = application.getAction("Save"); - final RgbButton saveButton = hwElements.getButton(CCAssignment.SAVE); - saveButton.bindPressed(mainLayer, () -> { - saveAction.invoke(); - lcdDisplay.sendPopup("", "Project Saved", KeylabIcon.COMPUTER); - }); - saveButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); - } - - - private RgbLightState getRecordingLightState() { - if (recordFocusMode == FocusMode.ARRANGER) { - return transport.isArrangerRecordEnabled().get() ? RgbLightState.RED : RgbLightState.RED_DIMMED; - } else { - return transport.isClipLauncherOverdubEnabled().get() ? RgbLightState.RED : RgbLightState.RED_DIMMED; - } - } - - private void handlePlayPressed() { - transport.play(); - } - - private void handleRecordPressed() { - if (recordFocusMode == FocusMode.ARRANGER) { - transport.isArrangerRecordEnabled().toggle(); - } else { - transport.isClipLauncherOverdubEnabled().toggle(); - } - } - - private void mainEncoderAction(final int dir) { - if (padMode == SysExHandler.PadMode.PAD_CLIPS) { - clipLaunchingLayer.navigateScenes(dir); - } else { - drumPadLayer.navigate(-dir); - } - } - - private void onMidi0(final ShortMidiMessage msg) { - final int sb = msg.getStatusByte() & (byte) 0xF0; - println("MIDI %02X %02X %02x %s", sb, msg.getData1(), msg.getData2(), mode); - } - - @Override - public void exit() { - final CompletableFuture shutdown = new CompletableFuture<>(); - Executors.newSingleThreadExecutor().execute(() -> { - sysExHandler.disconnectState(); - try { - Thread.sleep(100); - } catch (final InterruptedException e) { - e.printStackTrace(); - } - shutdown.complete(true); - }); - try { - shutdown.get(); - } catch (final InterruptedException | ExecutionException e) { - Thread.currentThread().interrupt(); - } - } - - @Override - public void flush() { - surface.updateHardware(); - } - - + + private Layer mainLayer; + + private HardwareSurface surface; + private ControllerHost host; + private Transport transport; + private SysExHandler sysExHandler; + + private Runnable nextPingAction = null; + private FocusMode recordFocusMode = FocusMode.ARRANGER; + private ClipLaunchingLayer clipLaunchingLayer; + private LcdDisplay lcdDisplay; + private MainScreenSection mainScreenSection; + private DrumPadLayer drumPadLayer; + private SysExHandler.PadMode padMode = SysExHandler.PadMode.PAD_CLIPS; + private boolean buttonPadRequestInvoked = false; + private LayoutType panelLayout = LayoutType.ARRANGER; + + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + private static ControllerHost debugHost; + private ViewControl viewControl; + + public static void println(final String format, final Object... args) { + if (debugHost != null) { + final LocalDateTime now = LocalDateTime.now(); + debugHost.println(now.format(DF) + " > " + String.format(format, args)); + } + } + + protected KeylabEssential3Extension(final KeyLabEssential3ExtensionDefinition definition, + final ControllerHost host) { + super(definition, host); + } + + @Override + public void init() { + host = getHost(); + debugHost = host; + final Context diContext = new Context(this); + surface = diContext.getService(HardwareSurface.class); + final MidiIn midiIn = host.getMidiInPort(0); + //midiIn.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi0); + final MidiOut midiOut = host.getMidiOutPort(0); + diContext.registerService(MidiIn.class, midiIn); + diContext.registerService(MidiOut.class, midiOut); + sysExHandler = diContext.getService(SysExHandler.class); + lcdDisplay = diContext.getService(LcdDisplay.class); + sysExHandler.addSysExEventListener(this::handleSysExEvent); + + final NoteInput noteInput = midiIn.createNoteInput( + "MIDI", getInputMask( + 0x0A, new int[] { + 0x1, 0x40, 0x72, 0x73, 0x1E, 0x1F, 0x56, 0x57, 0x49, 0x4B, 0x4F, 0x48, // + 0x50, 0x51, 0x52, 0x53, 0x55, 0x5A, 0x4A, 0x47, 0x4C, 0x4D, 0x5D, 0x12, 0x13, 0x10, 0x11 + })); + noteInput.setShouldConsumeEvents(true); + + mainLayer = diContext.createLayer("MAIN"); + clipLaunchingLayer = diContext.create(ClipLaunchingLayer.class); + drumPadLayer = diContext.create(DrumPadLayer.class); + viewControl = diContext.getService(ViewControl.class); + diContext.create(SliderEncoderControl.class); + mainScreenSection = diContext.getService(MainScreenSection.class); + diContext.create(BrowserLayer.class); + + sysExHandler.addPadModeEventListener(this::handlePadModeChanged); + + setUpTransportControl(diContext); + initEncoders(diContext); + initPadBankHandling(diContext); + + sysExHandler.deviceInquiry(); + + mainScreenSection.setPadInfo(drumPadLayer.getPadLocationInfo()); + mainLayer.activate(); + clipLaunchingLayer.activate(); + drumPadLayer.activate(); + + setUpPreferences(); + diContext.activate(); + host.scheduleTask(this::handlePing, 100); + } + + private void handleSysExEvent(final SysExHandler.SysexEventType sysexEventType) { + switch (sysexEventType) { + case DAW_MODE: + println(" Into Daw Mode"); + clipLaunchingLayer.activateIndication(true); + break; + case ARTURIA_MODE: + println(" Into Arturia Mode"); + break; + case USER_MODE: + println(" USER MODE"); + break; + case INIT: + lcdDisplay.logoText("Bitwig", "connected", KeylabIcon.BITWIG); + sysExHandler.requestPadBank(); + sysExHandler.queueTimedEvent(new TimedDelayEvent(() -> mainScreenSection.updatePage(), 2000)); + break; + } + } + + private void handlePing() { + lcdDisplay.ping(); + if (nextPingAction != null) { + nextPingAction.run(); + nextPingAction = null; + } + host.scheduleTask(this::handlePing, 100); + } + + private String[] getInputMask(final int excludeChannel, final int[] miniLabPassThroughCcs) { + final List masks = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + if (i != excludeChannel) { + masks.add(String.format("8%01x????", i)); + masks.add(String.format("9%01x????", i)); + } + } + masks.add("A?????"); // Poly Aftertouch + masks.add("D?????"); // Channel Aftertouch + masks.add("E?????"); // Pitchbend + masks.add("B1????"); // CCs Channel 2 + //masks.add("B0????"); + for (final int miniLabPassThroughCc : miniLabPassThroughCcs) { + masks.add(String.format("B0%02x??", miniLabPassThroughCc)); + } + return masks.toArray(String[]::new); + } + + private void setUpPreferences() { + final Preferences preferences = getHost().getPreferences(); // THIS + final SettableEnumValue recordButtonAssignment = preferences.getEnumSetting( + "Record Button assignment", // + "Transport", new String[] {FocusMode.LAUNCHER.getDescriptor(), FocusMode.ARRANGER.getDescriptor()}, + recordFocusMode.getDescriptor()); + recordButtonAssignment.addValueObserver(value -> recordFocusMode = FocusMode.toMode(value)); + final SettableEnumValue clipStopTiming = preferences.getEnumSetting( + "Long press to stop clip", // + "Clip", new String[] {"Fast", "Medium", "Standard"}, "Medium"); + clipStopTiming.addValueObserver(clipLaunchingLayer::setClipStopTiming); + } + + private void initEncoders(final Context diContext) { + final HwElements hwElements = diContext.getService(HwElements.class); + hwElements.bindEncoder(mainLayer, hwElements.getMainEncoder(), this::mainEncoderAction); + mainLayer.bindPressed(hwElements.getEncoderPress(), this::handleEncoderPressed); + } + + private void handlePadModeChanged(final SysExHandler.PadMode padMode) { + this.padMode = buttonPadRequestInvoked ? padMode.inverted() : padMode; + mainScreenSection.notifyPadMode(this.padMode); + buttonPadRequestInvoked = false; + } + + private void initPadBankHandling(final Context diContext) { + final HwElements hwElements = diContext.getService(HwElements.class); + final HardwareButton bankButton = hwElements.getBankButton(); + mainLayer.bindPressed( + bankButton, () -> { + sysExHandler.requestPadBank(); + buttonPadRequestInvoked = true; + }); + } + + private void handleEncoderPressed() { + if (padMode == SysExHandler.PadMode.PAD_CLIPS) { + clipLaunchingLayer.launchScene(); + } + } + + private void setUpTransportControl(final Context diContext) { + final HwElements hwElements = diContext.getService(HwElements.class); + transport = diContext.getService(Transport.class); + final Application application = diContext.getService(Application.class); + transport.isArrangerRecordEnabled().markInterested(); + transport.isClipLauncherOverdubEnabled().markInterested(); + application.panelLayout().addValueObserver(layout -> this.panelLayout = LayoutType.toType(layout)); + + final RgbButton loopButton = hwElements.getButton(CCAssignment.LOOP); + loopButton.bindToggle( + mainLayer, transport.isArrangerLoopEnabled(), RgbLightState.ORANGE, + RgbLightState.ORANGE_DIMMED); + final RgbButton playButton = hwElements.getButton(CCAssignment.PLAY); + transport.isPlaying().markInterested(); + playButton.bindPressed(mainLayer, this::handlePlayPressed); + playButton.bindLight( + mainLayer, + () -> transport.isPlaying().get() ? RgbLightState.GREEN : RgbLightState.GREEN_DIMMED); + + final RgbButton stopButton = hwElements.getButton(CCAssignment.STOP); + stopButton.bindPressed(mainLayer, () -> transport.stop()); + stopButton.bindLight( + mainLayer, + () -> transport.isPlaying().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); + + final RgbButton recordButton = hwElements.getButton(CCAssignment.REC); + recordButton.bindPressed(mainLayer, this::handleRecordPressed); + recordButton.bindLight(mainLayer, this::getRecordingLightState); + + final RgbButton tapButton = hwElements.getButton(CCAssignment.TAP); + tapButton.bind(mainLayer, transport.tapTempoAction(), RgbLightState.WHITE, RgbLightState.WHITE_DIMMED); + final RgbButton metroButton = hwElements.getButton(CCAssignment.METRO); + metroButton.bindToggle( + mainLayer, transport.isMetronomeEnabled(), RgbLightState.WHITE, + RgbLightState.WHITE_DIMMED); + + final RgbButton fastForwardButton = hwElements.getButton(CCAssignment.FFWD); + final RgbButton rewindButton = hwElements.getButton(CCAssignment.RWD); + fastForwardButton.bindRepeatHold(mainLayer, () -> transport.fastForward(), 400, 100); + fastForwardButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); + + rewindButton.bindRepeatHold(mainLayer, () -> transport.rewind(), 400, 100); + rewindButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); + + application.canUndo().markInterested(); + application.canRedo().markInterested(); + final RgbButton undoButton = hwElements.getButton(CCAssignment.UNDO); + undoButton.bindPressed(mainLayer, application::undo); + undoButton.bindLight( + mainLayer, + () -> application.canUndo().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); + + final RgbButton redoButton = hwElements.getButton(CCAssignment.REDO); + redoButton.bindPressed(mainLayer, application::redo); + redoButton.bindLight( + mainLayer, + () -> application.canRedo().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); + + // TODO Punch Button does nothing + final RgbButton quantizeButton = hwElements.getButton(CCAssignment.QUANTIZE); + quantizeButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); + quantizeButton.bindPressed(mainLayer, this::invokeQuantize); + // quantizeButton.bind(mainLayer, this::customAction, RgbLightState.WHITE, RgbLightState.WHITE_DIMMED); + final Action saveAction = application.getAction("Save"); + final RgbButton saveButton = hwElements.getButton(CCAssignment.SAVE); + saveButton.bindPressed( + mainLayer, () -> { + saveAction.invoke(); + lcdDisplay.sendPopup("", "Project Saved", KeylabIcon.COMPUTER); + }); + saveButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); + } + + + private RgbLightState getRecordingLightState() { + if (recordFocusMode == FocusMode.ARRANGER) { + return transport.isArrangerRecordEnabled().get() ? RgbLightState.RED : RgbLightState.RED_DIMMED; + } else { + return transport.isClipLauncherOverdubEnabled().get() ? RgbLightState.RED : RgbLightState.RED_DIMMED; + } + } + + private void handlePlayPressed() { + transport.play(); + } + + private void handleRecordPressed() { + if (recordFocusMode == FocusMode.ARRANGER) { + transport.isArrangerRecordEnabled().toggle(); + } else { + transport.isClipLauncherOverdubEnabled().toggle(); + } + } + + private void mainEncoderAction(final int dir) { + if (padMode == SysExHandler.PadMode.PAD_CLIPS) { + clipLaunchingLayer.navigateScenes(dir); + } else { + drumPadLayer.navigate(-dir); + } + } + + private void invokeQuantize() { + if (panelLayout == LayoutType.ARRANGER) { + final Clip clip = viewControl.getArrangerClip(); + if (clip.exists().get()) { + viewControl.invokeArrangerQuantize(); + lcdDisplay.sendPopup("Arrangement", "Clip Quantized", KeylabIcon.NONE); + } else { + lcdDisplay.sendPopup("Arrangement", "Quantization: No Clip", KeylabIcon.NONE); + } + } else { + final Clip clip = viewControl.getCursorClip(); + if (clip.exists().get()) { + viewControl.invokeLauncherQuantize(); + lcdDisplay.sendPopup("Launcher", "Clip Quantized", KeylabIcon.NONE); + } else { + lcdDisplay.sendPopup("Launcher", "Quantization: No Clip", KeylabIcon.NONE); + } + } + } + + @Override + public void exit() { + final CompletableFuture shutdown = new CompletableFuture<>(); + Executors.newSingleThreadExecutor().execute(() -> { + sysExHandler.disconnectState(); + try { + Thread.sleep(100); + } + catch (final InterruptedException e) { + e.printStackTrace(); + } + shutdown.complete(true); + }); + try { + shutdown.get(); + } + catch (final InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void flush() { + surface.updateHardware(); + } + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/PadBank.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/PadBank.java deleted file mode 100644 index 770b60d9..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/PadBank.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; - -public enum PadBank { - BANK_A(0, 0x30, 0x34), - BANK_B(1, 0x40, 0x44); - - private final int commandId; - private final int firstPadId; - private final int index; - - PadBank(final int index, final int commandId, final int firstPadId) { - this.index = index; - this.commandId = commandId; - this.firstPadId = firstPadId; - } - - public int getCommandId() { - return commandId; - } - - public int getIndex() { - return index; - } - - public int getFirstPadId() { - return firstPadId; - } -} diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/RgbButton.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/RgbButton.java index a3d40805..6dec00ed 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/RgbButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/RgbButton.java @@ -18,188 +18,189 @@ import com.bitwig.extensions.framework.time.TimedEvent; public class RgbButton { - private final byte[] rgbCommand = {(byte) 0xF0, 0x00, 0x20, 0x6B, 0x7F, 0x42, 0x04, // - 0x01, // 7 - Patch Id - 0x16, // 8 - Command - 0x02, // 9 - Pad ID - 0x00, // 10 - Red - 0x00, // 11 - Green - 0x00, // 12 - blue - 0x01, // state - (byte) 0xF7}; - - private final SysExHandler sysExHandler; - private final HardwareButton hwButton; - private final MultiStateHardwareLight light; - private final int padId; - private final int noteValue; - private TimedEvent currentTimer; - - public enum Type { - NOTE, - CC - } - - public RgbButton(final String name, final int padId, final Type type, final int value, final int channel, - final HardwareSurface surface, final SysExHandler sysExHandler) { - this.padId = padId; - noteValue = value; - this.sysExHandler = sysExHandler; - final MidiIn midiIn = sysExHandler.getMidiIn(); - sysExHandler.registerButton(this); - rgbCommand[9] = (byte) padId; - hwButton = surface.createHardwareButton(name + "_" + value); - if (type == Type.NOTE) { - hwButton.pressedAction().setActionMatcher(midiIn.createNoteOnActionMatcher(channel, value)); - hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, value)); - } else { - hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, value, 127)); - hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, value, 0)); - } - hwButton.isPressed().markInterested(); - light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + "_" + value); - light.setColorToStateFunction(RgbLightState::forColor); - hwButton.setBackgroundLight(light); - light.state().onUpdateHardware(this::updateState); -// if (bankId.getIndex() == -1) { // Individual updates handled -// light.state().onUpdateHardware(this::updateState); -// } - } - - public RgbButton(final CCAssignment hwElement, final Type type, final int channel, final SysExHandler sysExHandler, - final HardwareSurface surface) { - this("PAD_" + hwElement.toString(), hwElement.getItemId(), type, hwElement.getCcId(), channel, surface, - sysExHandler); - } - - public Integer getNoteValue() { - return noteValue; - } - - public MultiStateHardwareLight getLight() { - return light; - } - - private void updateState(final InternalHardwareLightState state) { - if (state instanceof final RgbLightState ligtState) { - ligtState.apply(rgbCommand); - sysExHandler.sendSysex(rgbCommand); - } else { - setRgbOff(); - sysExHandler.sendSysex(rgbCommand); - } - } - - public void forceDelayedRefresh() { - sysExHandler.sendDelayed(rgbCommand, 50); - } - - public void refresh() { - sysExHandler.sendSysex(rgbCommand); - } - - private void setRgbOff() { - rgbCommand[10] = 0; - rgbCommand[11] = 0; - rgbCommand[12] = 0; - } - - public void bindIsPressed(final Layer layer, final Consumer target) { - layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); - layer.bind(hwButton, hwButton.releasedAction(), () -> target.accept(false)); - } - - public void bindPressed(final Layer layer, final Runnable action) { - layer.bind(hwButton, hwButton.pressedAction(), action); - } - - public void bindPressed(final Layer layer, final Action action) { - layer.bind(hwButton, hwButton.pressedAction(), action); - } - - /** - * Binds the given action to a button. Upon pressing the button the action is immediately executed. However, while - * holding the button, the action repeats after an initial delay. - * - * @param layer the layer this is bound to - * @param pressedAction action to be invoked and after a delay repeat - * @param repeatDelay time in ms until the action gets repeated - * @param repeatFrequency time interval in ms between repeats - */ - public void bindRepeatHold(final Layer layer, final Runnable pressedAction, final int repeatDelay, - final int repeatFrequency) { - layer.bind(hwButton, hwButton.pressedAction(), () -> initiateRepeat(pressedAction, repeatDelay, repeatFrequency)); - layer.bind(hwButton, hwButton.releasedAction(), this::cancelEvent); - } - - /** - * Binds the given action to a button. Upon pressing the button the action is immediately executed. However, while - * holding the button, the action repeats after an initial delay. - * - * @param layer the layer this is bound to - * @param pressedAction only called once when button is pressed - * @param repeatAction the action to be repeated (also called on first down) - * @param releaseAction action callback when button released - * @param repeatDelay time in ms until the action gets repeated - * @param repeatFrequency time interval in ms between repeats - */ - public void bindRepeatHold(final Layer layer, final Runnable pressedAction, final Runnable repeatAction, - final Runnable releaseAction, final int repeatDelay, final int repeatFrequency) { - layer.bind(hwButton, hwButton.pressedAction(), () -> { - pressedAction.run(); - initiateRepeat(repeatAction, repeatDelay, repeatFrequency); - }); - layer.bind(hwButton, hwButton.releasedAction(), () -> { - cancelEvent(); - releaseAction.run(); - }); - } - - private void initiateRepeat(final Runnable action, final int repeatDelay, final int repeatFrequency) { - action.run(); - currentTimer = new TimeRepeatEvent(action, repeatDelay, repeatFrequency); - sysExHandler.queueTimedEvent(currentTimer); - } - - private void cancelEvent() { - if (currentTimer != null) { - currentTimer.cancel(); - currentTimer = null; - } - } - - public void bindReleased(final Layer layer, final Runnable action) { - layer.bind(hwButton, hwButton.releasedAction(), action); - } - - public void bindLight(final Layer layer, final Supplier lightSource) { - layer.bindLightState(lightSource::get, light); - } - - public void bindLight(final Layer layer, final RgbLightState color, final RgbLightState holdColor) { - hwButton.isPressed().markInterested(); - layer.bindLightState(() -> hwButton.isPressed().get() ? holdColor : color, light); - } - - - public void bind(final Layer layer, final Runnable action, final RgbLightState pressOn, - final RgbLightState pressOff) { - layer.bind(hwButton, hwButton.pressedAction(), action); - layer.bindLightState(() -> hwButton.isPressed().get() ? pressOn : pressOff, light); - } - - public void bind(final Layer layer, final HardwareActionBindable action, final RgbLightState pressOn, - final RgbLightState pressOff) { - layer.bind(hwButton, hwButton.pressedAction(), action); - layer.bindLightState(() -> hwButton.isPressed().get() ? pressOn : pressOff, light); - } - - - public void bindToggle(final Layer layer, final SettableBooleanValue value, final RgbLightState onColor, - final RgbLightState offColor) { - value.markInterested(); - layer.bind(hwButton, hwButton.pressedAction(), value::toggle); - layer.bindLightState(() -> value.get() ? onColor : offColor, light); - } - + private final byte[] rgbCommand = { + (byte) 0xF0, 0x00, 0x20, 0x6B, 0x7F, 0x42, 0x04, // + 0x01, // 7 - Patch Id + 0x16, // 8 - Command + 0x02, // 9 - Pad ID + 0x00, // 10 - Red + 0x00, // 11 - Green + 0x00, // 12 - blue + 0x01, // state + (byte) 0xF7 + }; + + private final SysExHandler sysExHandler; + private final HardwareButton hwButton; + private final MultiStateHardwareLight light; + private final int noteValue; + private TimedEvent currentTimer; + + public enum Type { + NOTE, + CC + } + + public RgbButton(final String name, final int padId, final Type type, final int value, final int channel, + final HardwareSurface surface, final SysExHandler sysExHandler) { + noteValue = value; + this.sysExHandler = sysExHandler; + final MidiIn midiIn = sysExHandler.getMidiIn(); + sysExHandler.registerButton(this); + rgbCommand[9] = (byte) padId; + hwButton = surface.createHardwareButton(name + "_" + value); + if (type == Type.NOTE) { + hwButton.pressedAction().setActionMatcher(midiIn.createNoteOnActionMatcher(channel, value)); + hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, value)); + } else { + hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, value, 127)); + hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, value, 0)); + } + hwButton.isPressed().markInterested(); + light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + "_" + value); + light.setColorToStateFunction(RgbLightState::forColor); + hwButton.setBackgroundLight(light); + light.state().onUpdateHardware(this::updateState); + } + + public RgbButton(final CCAssignment hwElement, final Type type, final int channel, final SysExHandler sysExHandler, + final HardwareSurface surface) { + this( + "PAD_" + hwElement.toString(), hwElement.getItemId(), type, hwElement.getCcId(), channel, surface, + sysExHandler); + } + + public Integer getNoteValue() { + return noteValue; + } + + public MultiStateHardwareLight getLight() { + return light; + } + + private void updateState(final InternalHardwareLightState state) { + if (state instanceof final RgbLightState lightState) { + lightState.apply(rgbCommand); + sysExHandler.sendSysex(rgbCommand); + } else { + setRgbOff(); + sysExHandler.sendSysex(rgbCommand); + } + } + + public void forceDelayedRefresh() { + sysExHandler.sendDelayed(rgbCommand, 50); + } + + public void refresh() { + sysExHandler.sendSysex(rgbCommand); + } + + private void setRgbOff() { + rgbCommand[10] = 0; + rgbCommand[11] = 0; + rgbCommand[12] = 0; + } + + public void bindIsPressed(final Layer layer, final Consumer target) { + layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> target.accept(false)); + } + + public void bindPressed(final Layer layer, final Runnable action) { + layer.bind(hwButton, hwButton.pressedAction(), action); + } + + public void bindPressed(final Layer layer, final Action action) { + layer.bind(hwButton, hwButton.pressedAction(), action); + } + + /** + * Binds the given action to a button. Upon pressing the button the action is immediately executed. However, while + * holding the button, the action repeats after an initial delay. + * + * @param layer the layer this is bound to + * @param pressedAction action to be invoked and after a delay repeat + * @param repeatDelay time in ms until the action gets repeated + * @param repeatFrequency time interval in ms between repeats + */ + public void bindRepeatHold(final Layer layer, final Runnable pressedAction, final int repeatDelay, + final int repeatFrequency) { + layer.bind( + hwButton, hwButton.pressedAction(), () -> initiateRepeat(pressedAction, repeatDelay, repeatFrequency)); + layer.bind(hwButton, hwButton.releasedAction(), this::cancelEvent); + } + + /** + * Binds the given action to a button. Upon pressing the button the action is immediately executed. However, while + * holding the button, the action repeats after an initial delay. + * + * @param layer the layer this is bound to + * @param pressedAction only called once when button is pressed + * @param repeatAction the action to be repeated (also called on first down) + * @param releaseAction action callback when button released + * @param repeatDelay time in ms until the action gets repeated + * @param repeatFrequency time interval in ms between repeats + */ + public void bindRepeatHold(final Layer layer, final Runnable pressedAction, final Runnable repeatAction, + final Runnable releaseAction, final int repeatDelay, final int repeatFrequency) { + layer.bind( + hwButton, hwButton.pressedAction(), () -> { + pressedAction.run(); + initiateRepeat(repeatAction, repeatDelay, repeatFrequency); + }); + layer.bind( + hwButton, hwButton.releasedAction(), () -> { + cancelEvent(); + releaseAction.run(); + }); + } + + private void initiateRepeat(final Runnable action, final int repeatDelay, final int repeatFrequency) { + action.run(); + currentTimer = new TimeRepeatEvent(action, repeatDelay, repeatFrequency); + sysExHandler.queueTimedEvent(currentTimer); + } + + private void cancelEvent() { + if (currentTimer != null) { + currentTimer.cancel(); + currentTimer = null; + } + } + + public void bindReleased(final Layer layer, final Runnable action) { + layer.bind(hwButton, hwButton.releasedAction(), action); + } + + public void bindLight(final Layer layer, final Supplier lightSource) { + layer.bindLightState(lightSource::get, light); + } + + public void bindLight(final Layer layer, final RgbLightState color, final RgbLightState holdColor) { + hwButton.isPressed().markInterested(); + layer.bindLightState(() -> hwButton.isPressed().get() ? holdColor : color, light); + } + + + public void bind(final Layer layer, final Runnable action, final RgbLightState pressOn, + final RgbLightState pressOff) { + layer.bind(hwButton, hwButton.pressedAction(), action); + layer.bindLightState(() -> hwButton.isPressed().get() ? pressOn : pressOff, light); + } + + public void bind(final Layer layer, final HardwareActionBindable action, final RgbLightState pressOn, + final RgbLightState pressOff) { + layer.bind(hwButton, hwButton.pressedAction(), action); + layer.bindLightState(() -> hwButton.isPressed().get() ? pressOn : pressOff, light); + } + + + public void bindToggle(final Layer layer, final SettableBooleanValue value, final RgbLightState onColor, + final RgbLightState offColor) { + value.markInterested(); + layer.bind(hwButton, hwButton.pressedAction(), value::toggle); + layer.bindLightState(() -> value.get() ? onColor : offColor, light); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/SliderEncoderControl.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/SliderEncoderControl.java index 0f8acf8a..18241244 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/SliderEncoderControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/SliderEncoderControl.java @@ -30,9 +30,11 @@ public class SliderEncoderControl extends Layer { private TrackBank mixerTrackBank; private boolean deviceScrollingOccurred = false; private boolean parameterSteppingOccurred = false; + private boolean trackBankScrollingOccurred = false; public enum State { - MIXER, DEVICE + MIXER, + DEVICE } private final ValueObject currentState = new ValueObject<>(State.MIXER); @@ -59,7 +61,8 @@ private void assignMixLayer(final HwElements hwElements) { final PinnableCursorDevice cursorDevice = viewControl.getCursorDevice(); cursorDevice.name().addValueObserver(name -> { if (deviceScrollingOccurred) { - lcdDisplay.sendPopup("Device Selected", // + lcdDisplay.sendPopup( + "Device Selected", // name, KeylabIcon.SFX_SMALL); } }); @@ -88,7 +91,8 @@ private void assignMixLayer(final HwElements hwElements) { mixerTrackBank.itemCount().markInterested(); mixerTrackBank.scrollPosition().addValueObserver(scrollPosition -> // lcdDisplay.sendPopup( - "Tracks", String.format("%d - %d", scrollPosition + 1, + "Tracks", String.format( + "%d - %d", scrollPosition + 1, Math.min(scrollPosition + 8, mixerTrackBank.itemCount().get())), KeylabIcon.NONE)); cursorDevice.name().markInterested(); @@ -104,24 +108,26 @@ private void assignMixLayer(final HwElements hwElements) { new KeyLabEncoderBinding(knobs[i], track.pan(), track.name(), LcdDisplayMode.PANNING, lcdDisplay)); } final CursorTrack cursorTrack = viewControl.getCursorTrack(); - addBinding(new KeyLabEncoderBinding(sliders[8], cursorTrack.volume(), cursorTrack.name(), LcdDisplayMode.VOLUME, + addBinding(new KeyLabEncoderBinding( + sliders[8], cursorTrack.volume(), cursorTrack.name(), LcdDisplayMode.VOLUME, lcdDisplay)); - addBinding(new KeyLabEncoderBinding(knobs[8], cursorTrack.pan(), cursorTrack.name(), LcdDisplayMode.PANNING, + addBinding(new KeyLabEncoderBinding( + knobs[8], cursorTrack.pan(), cursorTrack.name(), LcdDisplayMode.PANNING, lcdDisplay)); for (int i = 0; i < 8; i++) { final RemoteControl parameter4Knob = parameterBank1.getParameter(i); deviceControlLayer.addBinding( - new KeyLabEncoderBinding(knobs[i], parameter4Knob, parameter4Knob.name(), LcdDisplayMode.DEVICE1, - lcdDisplay)); + new KeyLabEncoderBinding( + knobs[i], parameter4Knob, parameter4Knob.name(), LcdDisplayMode.DEVICE1, lcdDisplay)); deviceControlLayer2.addBinding( - new KeyLabEncoderBinding(sliders[i], parameter4Knob, parameter4Knob.name(), LcdDisplayMode.DEVICE2, - lcdDisplay)); + new KeyLabEncoderBinding( + sliders[i], parameter4Knob, parameter4Knob.name(), LcdDisplayMode.DEVICE2, lcdDisplay)); final RemoteControl parameter4Slider = parameterBank2.getParameter(i); deviceControlLayer.addBinding( - new KeyLabEncoderBinding(sliders[i], parameter4Slider, parameter4Slider.name(), LcdDisplayMode.DEVICE2, - lcdDisplay)); + new KeyLabEncoderBinding( + sliders[i], parameter4Slider, parameter4Slider.name(), LcdDisplayMode.DEVICE2, lcdDisplay)); } } @@ -131,7 +137,8 @@ private void mainParameterBankIndexChanged(final CursorRemoteControlsPage parame } parameterBank2.selectedPageIndex().set((index + 1) % devicePageNames.length); if (parameterSteppingOccurred && !deviceScrollingOccurred) { - lcdDisplay.sendPopup(getParameterPageLabeling(index), // + lcdDisplay.sendPopup( + getParameterPageLabeling(index), // "Parameter Page " + (index + 1), KeylabIcon.SFX_SMALL); parameterSteppingOccurred = false; } @@ -163,9 +170,11 @@ private void initPartButton(final HwElements hwElements) { final RgbButton partButton = hwElements.getButton(CCAssignment.PART); partButton.bindPressed(this, this::handlePartDown); partButton.bindReleased(this, () -> handlePartRelease(partButton)); - partButton.bindLight(this, + partButton.bindLight( + this, () -> mixerTrackBank.scrollPosition().get() == 0 ? RgbLightState.WHITE_DIMMED : RgbLightState.WHITE); - partButton.bindLight(deviceControlLayer, + partButton.bindLight( + deviceControlLayer, () -> parameterBank1.selectedPageIndex().get() == 0 ? RgbLightState.WHITE_DIMMED : RgbLightState.WHITE); final RelativeHardwareKnob encoder = hwElements.getMainEncoder(); hwElements.bindEncoder(partModifierLayer, encoder, this::handlePartEncoder); @@ -182,6 +191,7 @@ private void handlePartEncoder(final int increment) { deviceScrollingOccurred = true; } else { mixerTrackBank.scrollBy(increment); + trackBankScrollingOccurred = true; } } @@ -197,7 +207,7 @@ public void handlePartRelease(final RgbButton button) { parameterSteppingOccurred = true; if (currentState.get() == State.DEVICE) { parameterBank1.selectNextPage(true); - } else { + } else if (!trackBankScrollingOccurred) { if (mixerTrackBank.scrollPosition().get() > 0) { mixerTrackBank.scrollPosition().set(0); } else { @@ -205,6 +215,7 @@ public void handlePartRelease(final RgbButton button) { } } } + trackBankScrollingOccurred = false; button.forceDelayedRefresh(); } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/HwElements.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/HwElements.java index d987d31e..7d6aacd0 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/HwElements.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/HwElements.java @@ -1,117 +1,128 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components; -import com.bitwig.extension.controller.api.*; +import java.util.HashMap; +import java.util.Map; +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareActionBindable; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.RelativeHardwareValueMatcher; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.CCAssignment; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.RgbButton; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.LcdDisplay; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.di.Component; -import java.util.HashMap; -import java.util.Map; -import java.util.function.IntConsumer; - @Component public class HwElements { - public static final int NUM_PADS_TRACK = 8; - private final Map buttons = new HashMap<>(); - private final KeylabAbsoluteControl[] knobs = new KeylabAbsoluteControl[9]; - private final KeylabAbsoluteControl[] sliders = new KeylabAbsoluteControl[9]; - private final RgbButton[] padBankAButtons = new RgbButton[NUM_PADS_TRACK]; - private final RgbButton[] padBankBButtons = new RgbButton[NUM_PADS_TRACK]; - private final RelativeHardwareKnob mainEncoder; - private final HardwareButton encoderPress; - - private final HardwareButton bankButton; - private final ControllerHost host; - - public HwElements(final ControllerHost host, final HardwareSurface surface, final SysExHandler sysExHandler, - MidiOut midiOut) { - this.host = host; - for (final CCAssignment assignment : CCAssignment.values()) { - if (!assignment.isMultiBase()) { - final RgbButton button = new RgbButton(assignment, RgbButton.Type.CC, 0, sysExHandler, surface); - buttons.put(assignment, button); - } - } - final MidiIn midiIn = sysExHandler.getMidiIn(); - mainEncoder = createMainEncoder(116, host, surface, midiIn); - encoderPress = createEncoderPress(117, surface, midiIn); - bankButton = surface.createHardwareButton("Bank_Button"); - bankButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(0, 0x76, 127)); - bankButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(0, 0x76, 0)); - - for (int i = 0; i < 9; i++) { - knobs[i] = new KeylabAbsoluteControl(LcdDisplay.ValueType.KNOB, i, surface, midiIn, midiOut); - sliders[i] = new KeylabAbsoluteControl(LcdDisplay.ValueType.SLIDER, i, surface, midiIn, midiOut); - } - final int[] mapping = new int[]{4, 5, 6, 7, 0, 1, 2, 3}; - for (int i = 0; i < 8; i++) { - padBankAButtons[i] = new RgbButton("KL_PAD_A", i + CCAssignment.PAD1_A.getItemId(), RgbButton.Type.NOTE, - CCAssignment.PAD1_A.getCcId() + mapping[i], 10, surface, sysExHandler); - padBankBButtons[i] = new RgbButton("KL_PAD_B", i + CCAssignment.PAD1_B.getItemId(), RgbButton.Type.NOTE, - CCAssignment.PAD1_B.getCcId() + i, 10, surface, sysExHandler); - } - } - - private RelativeHardwareKnob createMainEncoder(final int ccNr, final ControllerHost host, - final HardwareSurface surface, final MidiIn midiIn) { - final RelativeHardwareKnob mainEncoder = surface.createRelativeHardwareKnob("MAIN_ENCODER+_" + ccNr); - final RelativeHardwareValueMatcher stepUpMatcher = midiIn.createRelativeValueMatcher( - "(status == 176 && data1 == " + ccNr + " && data2>64)", 1); - final RelativeHardwareValueMatcher stepDownMatcher = midiIn.createRelativeValueMatcher( - "(status == 176 && data1 == " + ccNr + " && data2<64)", -1); - - final RelativeHardwareValueMatcher matcher = host.createOrRelativeHardwareValueMatcher(stepDownMatcher, - stepUpMatcher); - mainEncoder.setAdjustValueMatcher(matcher); - mainEncoder.setStepSize(1); - return mainEncoder; - } - - public void bindEncoder(final Layer layer, final RelativeHardwareKnob encoder, final IntConsumer action) { - final HardwareActionBindable incAction = host.createAction(() -> action.accept(1), () -> "+"); - final HardwareActionBindable decAction = host.createAction(() -> action.accept(-1), () -> "-"); - layer.bind(encoder, host.createRelativeHardwareControlStepTarget(incAction, decAction)); - } - - private HardwareButton createEncoderPress(final int ccNr, final HardwareSurface surface, final MidiIn midiIn) { - final HardwareButton encoderButton = surface.createHardwareButton("ENCODER_PUSH"); - - encoderButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(0, ccNr, 127)); - encoderButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(0, ccNr, 0)); - return encoderButton; - } - - public HardwareButton getBankButton() { - return bankButton; - } - - public RgbButton getButton(final CCAssignment assignment) { - return buttons.get(assignment); - } - - public RgbButton[] getPadBankAButtons() { - return padBankAButtons; - } - - public RgbButton[] getPadBankBButtons() { - return padBankBButtons; - } - - public KeylabAbsoluteControl[] getSliders() { - return sliders; - } - - public KeylabAbsoluteControl[] getKnobs() { - return knobs; - } - - public RelativeHardwareKnob getMainEncoder() { - return mainEncoder; - } - - public HardwareButton getEncoderPress() { - return encoderPress; - } + public static final int NUM_PADS_TRACK = 8; + private final Map buttons = new HashMap<>(); + private final KeylabAbsoluteControl[] knobs = new KeylabAbsoluteControl[9]; + private final KeylabAbsoluteControl[] sliders = new KeylabAbsoluteControl[9]; + private final RgbButton[] padBankAButtons = new RgbButton[NUM_PADS_TRACK]; + private final RgbButton[] padBankBButtons = new RgbButton[NUM_PADS_TRACK]; + private final RelativeHardwareKnob mainEncoder; + private final HardwareButton encoderPress; + + private final HardwareButton bankButton; + private final ControllerHost host; + + public HwElements(final ControllerHost host, final HardwareSurface surface, final SysExHandler sysExHandler, + final MidiOut midiOut) { + this.host = host; + for (final CCAssignment assignment : CCAssignment.values()) { + if (!assignment.isMultiBase()) { + final RgbButton button = new RgbButton(assignment, RgbButton.Type.CC, 0, sysExHandler, surface); + buttons.put(assignment, button); + } + } + final MidiIn midiIn = sysExHandler.getMidiIn(); + mainEncoder = createMainEncoder(116, host, surface, midiIn); + encoderPress = createEncoderPress(117, surface, midiIn); + bankButton = surface.createHardwareButton("Bank_Button"); + bankButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(0, 0x76, 127)); + bankButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(0, 0x76, 0)); + + for (int i = 0; i < 9; i++) { + knobs[i] = new KeylabAbsoluteControl(LcdDisplay.ValueType.KNOB, i, surface, midiIn, midiOut); + sliders[i] = new KeylabAbsoluteControl(LcdDisplay.ValueType.SLIDER, i, surface, midiIn, midiOut); + } + final int[] mapping = new int[] {4, 5, 6, 7, 0, 1, 2, 3}; + for (int i = 0; i < 8; i++) { + padBankAButtons[i] = + new RgbButton( + "KL_PAD_A", i + CCAssignment.PAD1_A.getItemId(), RgbButton.Type.NOTE, + CCAssignment.PAD1_A.getCcId() + mapping[i], 10, surface, sysExHandler); + padBankBButtons[i] = + new RgbButton( + "KL_PAD_B", i + CCAssignment.PAD1_B.getItemId(), RgbButton.Type.NOTE, + CCAssignment.PAD1_B.getCcId() + i, 10, surface, sysExHandler); + } + } + + private RelativeHardwareKnob createMainEncoder(final int ccNr, final ControllerHost host, + final HardwareSurface surface, final MidiIn midiIn) { + final RelativeHardwareKnob mainEncoder = surface.createRelativeHardwareKnob("MAIN_ENCODER+_" + ccNr); + final RelativeHardwareValueMatcher stepUpMatcher = + midiIn.createRelativeValueMatcher("(status == 176 && data1 == " + ccNr + " && data2>64)", 1); + final RelativeHardwareValueMatcher stepDownMatcher = + midiIn.createRelativeValueMatcher("(status == 176 && data1 == " + ccNr + " && data2<64)", -1); + + final RelativeHardwareValueMatcher matcher = + host.createOrRelativeHardwareValueMatcher(stepDownMatcher, stepUpMatcher); + mainEncoder.setAdjustValueMatcher(matcher); + mainEncoder.setStepSize(1); + return mainEncoder; + } + + public void bindEncoder(final Layer layer, final RelativeHardwareKnob encoder, final IntConsumer action) { + final HardwareActionBindable incAction = host.createAction(() -> action.accept(1), () -> "+"); + final HardwareActionBindable decAction = host.createAction(() -> action.accept(-1), () -> "-"); + layer.bind(encoder, host.createRelativeHardwareControlStepTarget(incAction, decAction)); + } + + private HardwareButton createEncoderPress(final int ccNr, final HardwareSurface surface, final MidiIn midiIn) { + final HardwareButton encoderButton = surface.createHardwareButton("ENCODER_PUSH"); + + encoderButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(0, ccNr, 127)); + encoderButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(0, ccNr, 0)); + return encoderButton; + } + + public HardwareButton getBankButton() { + return bankButton; + } + + public RgbButton getButton(final CCAssignment assignment) { + return buttons.get(assignment); + } + + public RgbButton[] getPadBankAButtons() { + return padBankAButtons; + } + + public RgbButton[] getPadBankBButtons() { + return padBankBButtons; + } + + public KeylabAbsoluteControl[] getSliders() { + return sliders; + } + + public KeylabAbsoluteControl[] getKnobs() { + return knobs; + } + + public RelativeHardwareKnob getMainEncoder() { + return mainEncoder; + } + + public HardwareButton getEncoderPress() { + return encoderPress; + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/KeyLabEncoderBinding.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/KeyLabEncoderBinding.java index 4d5f1884..c5d9e261 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/KeyLabEncoderBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/KeyLabEncoderBinding.java @@ -1,73 +1,79 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.AbsoluteHardwareControlBinding; +import com.bitwig.extension.controller.api.HardwareBinding; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.StringValue; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.LcdDisplay; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.LcdDisplayMode; import com.bitwig.extensions.framework.Binding; public class KeyLabEncoderBinding extends Binding { - private final LcdDisplay display; - private HardwareBinding hwBinding; - private final KeylabAbsoluteControl control; - private int currentValue = 0; - private boolean targetExists = false; - - public KeyLabEncoderBinding(final KeylabAbsoluteControl control, final Parameter target, - final StringValue nameSource, final LcdDisplayMode mode, final LcdDisplay display) { - super(control.getControl(), control.getControl(), target); - this.control = control; - this.display = display; - target.displayedValue().markInterested(); - target.name().markInterested(); - target.value().markInterested(); - nameSource.markInterested(); - final int index = control.getIndex() + 1; - getSource().value().addValueObserver(val -> handleControlValueChanged(mode, val)); - - target.exists().addValueObserver(exists -> { - targetExists = exists; - if (isActive()) { - control.forceValue(exists); - } - }); - - target.value().addValueObserver(128, v -> { - currentValue = v; - if (!isActive()) { + private final LcdDisplay display; + private HardwareBinding hwBinding; + private final KeylabAbsoluteControl control; + private int currentValue = 0; + private boolean targetExists = false; + + public KeyLabEncoderBinding(final KeylabAbsoluteControl control, final Parameter target, + final StringValue nameSource, final LcdDisplayMode mode, final LcdDisplay display) { + super(control.getControl(), control.getControl(), target); + this.control = control; + this.display = display; + target.displayedValue().markInterested(); + target.name().markInterested(); + target.value().markInterested(); + nameSource.markInterested(); + final int index = control.getIndex() + 1; + getSource().value().addValueObserver(val -> handleControlValueChanged(mode, val)); + + target.exists().addValueObserver(exists -> { + targetExists = exists; + if (isActive()) { + control.forceValue(exists); + } + }); + + target.value().addValueObserver( + 128, v -> { + currentValue = v; + if (!isActive()) { + return; + } + display.sendValueText( + index, control.getValueType(), mode, nameSource.get(), target.displayedValue().get(), v); + control.updateValue(currentValue); + }); + } + + private void handleControlValueChanged(final LcdDisplayMode mode, final double value) { + if (!isActive()) { return; - } - display.sendValueText(index, control.getValueType(), mode, nameSource.get(), target.displayedValue().get(), v); - control.updateValue(currentValue); - }); - } - - private void handleControlValueChanged(LcdDisplayMode mode, double value) { - if (!isActive()) { - return; - } - int index = control.getIndex() + 1; - display.enableValues(index, mode); - } - - - @Override - protected void activate() { - assert hwBinding == null; - hwBinding = getHardwareBinding(); - control.updateValue(currentValue); - control.forceValue(targetExists); - } - - @Override - protected void deactivate() { - assert hwBinding != null; - - hwBinding.removeBinding(); - hwBinding = null; - } - - protected AbsoluteHardwareControlBinding getHardwareBinding() { - return getTarget().addBinding(getSource()); - } - + } + final int index = control.getIndex() + 1; + display.enableValues(index, mode); + } + + + @Override + protected void activate() { + assert hwBinding == null; + hwBinding = getHardwareBinding(); + control.updateValue(currentValue); + control.forceValue(targetExists); + } + + @Override + protected void deactivate() { + assert hwBinding != null; + + hwBinding.removeBinding(); + hwBinding = null; + } + + protected AbsoluteHardwareControlBinding getHardwareBinding() { + return getTarget().addBinding(getSource()); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/KeylabAbsoluteControl.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/KeylabAbsoluteControl.java index 78427f23..0f423fdb 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/KeylabAbsoluteControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/KeylabAbsoluteControl.java @@ -7,58 +7,58 @@ import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.LcdDisplay; public class KeylabAbsoluteControl { - public static final int ENCODER_CC = 96; - public static final int SLIDER_CC = 105; - private static final int ENCODER_COMPONENT_ID = 3; - private static final String SYS_EX_STATE = "F0 00 20 6B 7F 42 02 0F 40 %02X %02X F7"; - private static final String SYS_FORCE_VALUE = "F0 00 20 6B 7F 42 04 01 60 21 03 %02X 00 F7"; - - private AbsoluteHardwareControl control; - private LcdDisplay.ValueType valueType; - private int index; - private int ccNr; - private MidiOut midiOut; - - public KeylabAbsoluteControl(LcdDisplay.ValueType valueType, int index, HardwareSurface surface, MidiIn midiIn, - MidiOut midiOut) { - this.valueType = valueType; - this.index = index; - this.midiOut = midiOut; - - if (valueType == LcdDisplay.ValueType.KNOB) { - this.ccNr = ENCODER_CC + index; - control = surface.createAbsoluteHardwareKnob("KNOB_" + (index + 1)); - control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(0, this.ccNr)); - } else { - this.ccNr = SLIDER_CC + index; - control = surface.createHardwareSlider("FADER_" + (index + 1)); - control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(0, this.ccNr)); - } - } - - public AbsoluteHardwareControl getControl() { - return control; - } - - public int getIndex() { - return index; - } - - public LcdDisplay.ValueType getValueType() { - return valueType; - } - - public void updateValue(int currentValue) { - if (valueType == LcdDisplay.ValueType.KNOB) { - midiOut.sendSysex(String.format(SYS_EX_STATE, index + ENCODER_COMPONENT_ID, currentValue)); - } - } - - public void forceValue(boolean active) { - if (!active) { - midiOut.sendSysex(String.format(SYS_FORCE_VALUE, this.ccNr)); - } else { - midiOut.sendSysex(String.format("F0 00 20 6B 7F 42 04 01 60 20 03 %02X 00 F7", this.ccNr)); - } - } + public static final int ENCODER_CC = 96; + public static final int SLIDER_CC = 105; + private static final int ENCODER_COMPONENT_ID = 3; + private static final String SYS_EX_STATE = "F0 00 20 6B 7F 42 02 0F 40 %02X %02X F7"; + private static final String SYS_FORCE_VALUE = "F0 00 20 6B 7F 42 04 01 60 21 03 %02X 00 F7"; + + private final AbsoluteHardwareControl control; + private final LcdDisplay.ValueType valueType; + private final int index; + private final int ccNr; + private final MidiOut midiOut; + + public KeylabAbsoluteControl(final LcdDisplay.ValueType valueType, final int index, final HardwareSurface surface, final MidiIn midiIn, + final MidiOut midiOut) { + this.valueType = valueType; + this.index = index; + this.midiOut = midiOut; + + if (valueType == LcdDisplay.ValueType.KNOB) { + this.ccNr = ENCODER_CC + index; + control = surface.createAbsoluteHardwareKnob("KNOB_" + (index + 1)); + control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(0, this.ccNr)); + } else { + this.ccNr = SLIDER_CC + index; + control = surface.createHardwareSlider("FADER_" + (index + 1)); + control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(0, this.ccNr)); + } + } + + public AbsoluteHardwareControl getControl() { + return control; + } + + public int getIndex() { + return index; + } + + public LcdDisplay.ValueType getValueType() { + return valueType; + } + + public void updateValue(final int currentValue) { + if (valueType == LcdDisplay.ValueType.KNOB) { + midiOut.sendSysex(String.format(SYS_EX_STATE, index + ENCODER_COMPONENT_ID, currentValue)); + } + } + + public void forceValue(final boolean active) { + if (!active) { + midiOut.sendSysex(String.format(SYS_FORCE_VALUE, this.ccNr)); + } else { + midiOut.sendSysex(String.format("F0 00 20 6B 7F 42 04 01 60 20 03 %02X 00 F7", this.ccNr)); + } + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/SysExHandler.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/SysExHandler.java index 8bdbacde..b475131b 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/SysExHandler.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/SysExHandler.java @@ -1,5 +1,12 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.function.Consumer; + import com.bitwig.extension.controller.api.ControllerHost; import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.MidiOut; @@ -12,229 +19,212 @@ import com.bitwig.extensions.framework.time.TimedDelayEvent; import com.bitwig.extensions.framework.time.TimedEvent; -import java.util.*; -import java.util.function.Consumer; - @Component public class SysExHandler { - - //public static final String ARTURIA_CLEAR_SCREEN = "f0 00 20 6B 7f 42 04 01 60 0a 0a 5f 51 00 f7"; - public static final String ARTURIA_SYSEX_HEADER = "f0 00 20 6B 7f 42 "; - public static final String SYSEX_DEVICE_RECOGNITION = "f07e7f060200206b02"; - public static final String PAD_MODE_HEADER = "f000206b7f422111400000"; - public static final String MODE_CHANGE_HEADER = "f000206b7f422111400200"; - - public enum GeneralMode { - DAW_MODE, - ANALOG_LAB - } - - public enum SysexEventType { - DAW_MODE, - ARTURIA_MODE, - USER_MODE, - INIT - } - - public enum PadMode { - PAD_CLIPS, - PAD_DRUMS; - - public PadMode inverted() { - return this == PAD_CLIPS ? PAD_DRUMS : PAD_CLIPS; - } - } - - @Inject - private MidiOut midiOut; - @Inject - private ControllerHost host; - private final MidiIn midiIn; - - private final List buttons = new ArrayList<>(); - private final List> sysExEventListener = new ArrayList<>(); - private final List> padModeEventListener = new ArrayList<>(); - private final Queue sysExTask = new LinkedList<>(); - private final Queue timedEvents = new LinkedList<>(); - private final List tickListeners = new ArrayList<>(); - - private final long creationTime; - private boolean processingReady = false; - private boolean midiProcessingRunning; - - public SysExHandler(final MidiIn midiIn) { - this.midiIn = midiIn; - midiIn.setSysexCallback(this::handleSysExData); - creationTime = System.currentTimeMillis(); - } - - @Activate - public void activate() { - host.scheduleTask(this::processMidi, 0); - midiProcessingRunning = true; - } - - public MidiIn getMidiIn() { - return midiIn; - } - - public void processMidi() { - if (processingReady && !sysExTask.isEmpty()) { - while (!sysExTask.isEmpty()) { - sysExTask.poll().run(); -// if (!sysExTask.isEmpty()) { -// pause(1); -// } - } - } - if (!timedEvents.isEmpty()) { - Iterator it = timedEvents.iterator(); - while (it.hasNext()) { - final TimedEvent event = it.next(); - event.process(); - if (event.isCompleted()) { - it.remove(); + + public static final String ARTURIA_SYSEX_HEADER = "f0 00 20 6B 7f 42 "; + public static final String SYSEX_DEVICE_RECOGNITION = "f07e7f060200206b02"; + public static final String PAD_MODE_HEADER = "f000206b7f422111400000"; + public static final String MODE_CHANGE_HEADER = "f000206b7f422111400200"; + + public enum SysexEventType { + DAW_MODE, + ARTURIA_MODE, + USER_MODE, + INIT + } + + public enum PadMode { + PAD_CLIPS, + PAD_DRUMS; + + public PadMode inverted() { + return this == PAD_CLIPS ? PAD_DRUMS : PAD_CLIPS; + } + } + + @Inject + private MidiOut midiOut; + @Inject + private ControllerHost host; + private final MidiIn midiIn; + + private final List buttons = new ArrayList<>(); + private final List> sysExEventListener = new ArrayList<>(); + private final List> padModeEventListener = new ArrayList<>(); + private final Queue sysExTask = new LinkedList<>(); + private final Queue timedEvents = new LinkedList<>(); + private final List tickListeners = new ArrayList<>(); + + private final long creationTime; + private boolean processingReady = false; + private boolean midiProcessingRunning; + + public SysExHandler(final MidiIn midiIn) { + this.midiIn = midiIn; + midiIn.setSysexCallback(this::handleSysExData); + creationTime = System.currentTimeMillis(); + } + + @Activate + public void activate() { + host.scheduleTask(this::processMidi, 0); + midiProcessingRunning = true; + } + + public MidiIn getMidiIn() { + return midiIn; + } + + public void processMidi() { + if (processingReady && !sysExTask.isEmpty()) { + while (!sysExTask.isEmpty()) { + sysExTask.poll().run(); } - } - } - if (!processingReady) { - final long diff = System.currentTimeMillis() - creationTime; - if (diff > 1000) { - KeylabEssential3Extension.println(" Not Connected after %d ms", diff); - disconnectState(); - midiProcessingRunning = false; - pause(100); - deviceInquiry(); - return; - } - } - tickListeners.forEach(Runnable::run); - host.scheduleTask(this::processMidi, 10); - } - - public void registerTickTask(Runnable task) { - tickListeners.add(task); - } - - public void requestPadBank() { - midiOut.sendSysex(ARTURIA_SYSEX_HEADER + "01 11 40 00 F7"); - } - - public void sendDelayed(final byte[] sysExByteCommand, final int waitTime) { - timedEvents.add(new TimedDelayEvent(() -> midiOut.sendSysex(sysExByteCommand), waitTime)); - } - - public void queueTimedEvent(final TimedEvent timedEvent) { - timedEvents.add(timedEvent); - } - - private void handleSysExData(final String sysEx) { - if (sysEx.startsWith(SYSEX_DEVICE_RECOGNITION)) { - final String value = extractSysexRest(sysEx, MODE_CHANGE_HEADER); - final long diff = System.currentTimeMillis() - creationTime; - KeylabEssential3Extension.println("Device Inquiry Response = %s after %d ms MIDI Processing=%s", value, diff, - midiProcessingRunning); - requestInitState(); - pause(30); - notify(SysexEventType.INIT); - if (!midiProcessingRunning) { - KeylabEssential3Extension.println(" REACTIVATING "); - activate(); - } - } else if (sysEx.startsWith(MODE_CHANGE_HEADER)) { - final String mode = extractSysexRest(sysEx, MODE_CHANGE_HEADER); - KeylabEssential3Extension.println(" MODE = %s", mode); - if ("01".equals(mode)) { - notify(SysexEventType.DAW_MODE); - } else if ("00".equals(mode)) { - notify(SysexEventType.ARTURIA_MODE); - } else if ("02".equals(mode)) { - notify(SysexEventType.USER_MODE); - } - } else if (sysEx.startsWith(PAD_MODE_HEADER)) { - final String mode = extractSysexRest(sysEx, PAD_MODE_HEADER); - if (mode.equals("01")) { - padModeEventListener.forEach(e -> e.accept(PadMode.PAD_DRUMS)); - } else { - padModeEventListener.forEach(e -> e.accept(PadMode.PAD_CLIPS)); - } - } else { - KeylabEssential3Extension.println("Unknown Received SysEx : %s", sysEx); - } - } - - private String extractSysexRest(final String sysEx, final String header) { - return sysEx.substring(header.length(), sysEx.length() - 2); - } - - public void registerButton(final RgbButton button) { - buttons.add(button); - } - - private void notify(final SysexEventType event) { - sysExEventListener.forEach(e -> e.accept(event)); - } - - public void addSysExEventListener(final Consumer listener) { - sysExEventListener.add(listener); - } - - public void addPadModeEventListener(final Consumer listener) { - padModeEventListener.add(listener); - } - - public void deviceInquiry() { - midiOut.sendSysex("f0 7e 7f 06 01 f7"); // Universal Request - } - - public void requestInitState() { - midiOut.sendSysex("f0 00 20 6B 7f 42 02 0F 40 5A 01 F7"); - processingReady = true; - } - - public void disconnectState() { - KeylabEssential3Extension.println(" Disconnect "); - midiOut.sendSysex("f0 00 20 6B 7f 42 02 0F 40 5A 00 F7"); - processingReady = false; - pause(20); - } - - public void sendRgb(final CCAssignment hwElement, final int red, final int green, final int blue) { - final StringBuilder sysex = new StringBuilder(ARTURIA_SYSEX_HEADER); - sysex.append("04 01 "); // Command ID + Patch ID - sysex.append("16 "); // paramtype - sysex.append(toHex(hwElement.getItemId())); - sysex.append(toHex(red)); - sysex.append(toHex(green)); - sysex.append(toHex(blue)); - sysex.append("F7"); - sysExTask.add(() -> midiOut.sendSysex(sysex.toString())); - } - - public void sendSysex(final byte[] sysExExpression) { - sysExTask.add(() -> midiOut.sendSysex(sysExExpression)); - } - - public void sendSysex(final String sysExExpression) { - sysExTask.add(() -> midiOut.sendSysex(sysExExpression)); - } - - public void sendSysexText(final String sysExExpression) { - sysExTask.add(() -> midiOut.sendSysex(sysExExpression)); - } - - public static String toHex(final int values) { - final String hexValue = Integer.toHexString((byte) values); - return (hexValue.length() < 2 ? "0" + hexValue : hexValue) + " "; - } - - void pause(final int millis) { - try { - Thread.sleep(millis); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - + } + if (!timedEvents.isEmpty()) { + final Iterator it = timedEvents.iterator(); + while (it.hasNext()) { + final TimedEvent event = it.next(); + event.process(); + if (event.isCompleted()) { + it.remove(); + } + } + } + if (!processingReady) { + final long diff = System.currentTimeMillis() - creationTime; + if (diff > 1000) { + KeylabEssential3Extension.println(" Not Connected after %d ms", diff); + disconnectState(); + midiProcessingRunning = false; + pause(100); + deviceInquiry(); + return; + } + } + tickListeners.forEach(Runnable::run); + host.scheduleTask(this::processMidi, 10); + } + + public void registerTickTask(final Runnable task) { + tickListeners.add(task); + } + + public void requestPadBank() { + midiOut.sendSysex(ARTURIA_SYSEX_HEADER + "01 11 40 00 F7"); + } + + public void sendDelayed(final byte[] sysExByteCommand, final int waitTime) { + timedEvents.add(new TimedDelayEvent(() -> midiOut.sendSysex(sysExByteCommand), waitTime)); + } + + public void queueTimedEvent(final TimedEvent timedEvent) { + timedEvents.add(timedEvent); + } + + private void handleSysExData(final String sysEx) { + if (sysEx.startsWith(SYSEX_DEVICE_RECOGNITION)) { + final String value = extractSysexRest(sysEx, MODE_CHANGE_HEADER); + final long diff = System.currentTimeMillis() - creationTime; + KeylabEssential3Extension.println( + "Device Inquiry Response = %s after %d ms MIDI Processing=%s", value, + diff, midiProcessingRunning); + requestInitState(); + pause(30); + notify(SysexEventType.INIT); + if (!midiProcessingRunning) { + KeylabEssential3Extension.println(" REACTIVATING "); + activate(); + } + } else if (sysEx.startsWith(MODE_CHANGE_HEADER)) { + final String mode = extractSysexRest(sysEx, MODE_CHANGE_HEADER); + KeylabEssential3Extension.println(" MODE = %s", mode); + switch (mode) { + case "01" -> notify(SysexEventType.DAW_MODE); + case "00" -> notify(SysexEventType.ARTURIA_MODE); + case "02" -> notify(SysexEventType.USER_MODE); + } + } else if (sysEx.startsWith(PAD_MODE_HEADER)) { + final String mode = extractSysexRest(sysEx, PAD_MODE_HEADER); + if (mode.equals("01")) { + padModeEventListener.forEach(e -> e.accept(PadMode.PAD_DRUMS)); + } else { + padModeEventListener.forEach(e -> e.accept(PadMode.PAD_CLIPS)); + } + } else { + KeylabEssential3Extension.println("Unknown Received SysEx : %s", sysEx); + } + } + + private String extractSysexRest(final String sysEx, final String header) { + return sysEx.substring(header.length(), sysEx.length() - 2); + } + + public void registerButton(final RgbButton button) { + buttons.add(button); + } + + private void notify(final SysexEventType event) { + sysExEventListener.forEach(e -> e.accept(event)); + } + + public void addSysExEventListener(final Consumer listener) { + sysExEventListener.add(listener); + } + + public void addPadModeEventListener(final Consumer listener) { + padModeEventListener.add(listener); + } + + public void deviceInquiry() { + midiOut.sendSysex("f0 7e 7f 06 01 f7"); // Universal Request + } + + public void requestInitState() { + midiOut.sendSysex("f0 00 20 6B 7f 42 02 0F 40 5A 01 F7"); + processingReady = true; + } + + public void disconnectState() { + KeylabEssential3Extension.println(" Disconnect "); + midiOut.sendSysex("f0 00 20 6B 7f 42 02 0F 40 5A 00 F7"); + processingReady = false; + pause(20); + } + + public void sendRgb(final CCAssignment hwElement, final int red, final int green, final int blue) { + final String sysex = ARTURIA_SYSEX_HEADER + "04 01 " // Command ID + Patch ID + + "16 " // Parameter Type + + toHex(hwElement.getItemId()) + toHex(red) + toHex(green) + toHex(blue) + "F7"; + sysExTask.add(() -> midiOut.sendSysex(sysex)); + } + + public void sendSysex(final byte[] sysExExpression) { + sysExTask.add(() -> midiOut.sendSysex(sysExExpression)); + } + + public void sendSysex(final String sysExExpression) { + sysExTask.add(() -> midiOut.sendSysex(sysExExpression)); + } + + public void sendSysexText(final String sysExExpression) { + sysExTask.add(() -> midiOut.sendSysex(sysExExpression)); + } + + public static String toHex(final int values) { + final String hexValue = Integer.toHexString((byte) values); + return (hexValue.length() < 2 ? "0" + hexValue : hexValue) + " "; + } + + void pause(final int millis) { + try { + Thread.sleep(millis); + } + catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/ViewControl.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/ViewControl.java index 04a587b2..d2a8f0dd 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/ViewControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/components/ViewControl.java @@ -12,7 +12,6 @@ import com.bitwig.extension.controller.api.PinnableCursorDevice; import com.bitwig.extension.controller.api.Scene; import com.bitwig.extension.controller.api.TrackBank; -import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.LcdDisplay; import com.bitwig.extensions.framework.di.Component; import com.bitwig.extensions.framework.di.PostConstruct; import com.bitwig.extensions.framework.values.BooleanValueObject; @@ -29,8 +28,9 @@ public class ViewControl { private final Scene sceneTrackItem; private final PinnableCursorDevice cursorDevice; private final Clip cursorClip; + private final Clip arrangerClip; - public ViewControl(final ControllerHost host, final LcdDisplay lcdDisplay) { + public ViewControl(final ControllerHost host) { mixerTrackBank = host.createTrackBank(8, 2, 2); cursorTrack = host.createCursorTrack(2, 2); @@ -41,11 +41,17 @@ public ViewControl(final ControllerHost host, final LcdDisplay lcdDisplay) { sceneTrackItem = viewTrackBank.sceneBank().getScene(0); cursorClip = host.createLauncherCursorClip(32, 127); - primaryDevice = cursorTrack.createCursorDevice("DrumDetection", "Pad Device", NUM_PADS_TRACK, + primaryDevice = cursorTrack.createCursorDevice( + "DrumDetection", "Pad Device", NUM_PADS_TRACK, CursorDeviceFollowMode.FIRST_INSTRUMENT); - cursorDevice = cursorTrack.createCursorDevice("device-control", "Device Control", 0, + cursorDevice = cursorTrack.createCursorDevice( + "device-control", "Device Control", 0, CursorDeviceFollowMode.FOLLOW_SELECTION); - //cursorDevice = cursorTrack.createCursorDevice(); + arrangerClip = host.createArrangerCursorClip(32, 128); + + cursorClip.exists().markInterested(); + cursorClip.clipLauncherSlot().name().markInterested(); + arrangerClip.exists().markInterested(); setUpFollowArturiaDevice(host); } @@ -66,12 +72,6 @@ private void setUpFollowArturiaDevice(final ControllerHost host) { controlsAnalogLab = new BooleanValueObject(); - // controlsAnalogLab.addValueObserver(controlsLab -> sysExHandler.fireArturiaMode( - // controlsLab ? com.bitwig.extensions.controllers.arturia.minilab3.SysExHandler.GeneralMode - // .ANALOG_LAB : com.bitwig.extensions.controllers.arturia.minilab3.SysExHandler.GeneralMode - // .DAW_MODE, - // arturiaModeLayer.isActive())); - final BooleanValue onArturiaDevice = cursorDevice.createEqualsValue(matcherDevice); cursorTrack.arm().addValueObserver( armed -> controlsAnalogLab.set(armed && cursorDevice.exists().get() && onArturiaDevice.get())); @@ -105,13 +105,25 @@ public TrackBank getMixerTrackBank() { return mixerTrackBank; } - public BooleanValueObject getControlsAnalogLab() { - return controlsAnalogLab; + public Clip getArrangerClip() { + return arrangerClip; } - public void invokeQuantize() { + public Clip getCursorClip() { + return cursorClip; + } + + public void invokeLauncherQuantize() { cursorClip.quantize(1.0); final ClipLauncherSlot slot = cursorClip.clipLauncherSlot(); slot.showInEditor(); } + + public void invokeArrangerQuantize() { + arrangerClip.quantize(1.0); + final ClipLauncherSlot slot = arrangerClip.clipLauncherSlot(); + slot.showInEditor(); + } + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/FooterIconDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/FooterIconDisplayBinding.java index 8f065e8a..d4a9f07c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/FooterIconDisplayBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/FooterIconDisplayBinding.java @@ -4,39 +4,39 @@ import com.bitwig.extensions.framework.Binding; public class FooterIconDisplayBinding extends Binding { - - private final LcdDisplay display; - private final ContextPart.FrameType frameTypeActive; - private final ContextPart.FrameType frameTypeInactive; - private final int footerIndex; - - public FooterIconDisplayBinding(ContextPageConfiguration target, LcdDisplay display, BooleanValue source, - int footerIndex, ContextPart.FrameType frameTypeActive, - ContextPart.FrameType frameTypeInactive) { - super(source, source, target); - this.display = display; - this.frameTypeActive = frameTypeActive; - this.frameTypeInactive = frameTypeInactive; - this.footerIndex = footerIndex; - source.addValueObserver(this::handleValueChanged); - } - - private void handleValueChanged(boolean active) { - if (!isActive()) { - return; - } - getTarget().setFramed(footerIndex, active ? frameTypeActive : frameTypeInactive); - display.updateFooter(getTarget()); - } - - @Override - protected void deactivate() { - // nothing to do - } - - @Override - protected void activate() { - getTarget().setFramed(footerIndex, getSource().get() ? frameTypeActive : frameTypeInactive); - display.updateFooter(getTarget()); - } + + private final LcdDisplay display; + private final ContextPart.FrameType frameTypeActive; + private final ContextPart.FrameType frameTypeInactive; + private final int footerIndex; + + public FooterIconDisplayBinding(final ContextPageConfiguration target, final LcdDisplay display, + final BooleanValue source, final int footerIndex, final ContextPart.FrameType frameTypeActive, + final ContextPart.FrameType frameTypeInactive) { + super(source, source, target); + this.display = display; + this.frameTypeActive = frameTypeActive; + this.frameTypeInactive = frameTypeInactive; + this.footerIndex = footerIndex; + source.addValueObserver(this::handleValueChanged); + } + + private void handleValueChanged(final boolean active) { + if (!isActive()) { + return; + } + getTarget().setFramed(footerIndex, active ? frameTypeActive : frameTypeInactive); + display.updateFooter(getTarget()); + } + + @Override + protected void deactivate() { + // nothing to do + } + + @Override + protected void activate() { + getTarget().setFramed(footerIndex, getSource().get() ? frameTypeActive : frameTypeInactive); + display.updateFooter(getTarget()); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/LcdDisplay.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/LcdDisplay.java index 23726e46..971087d0 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/LcdDisplay.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/LcdDisplay.java @@ -7,267 +7,254 @@ @Component public class LcdDisplay { - public static final int MAX_TEXT_LEN = 20; - - private final SysExHandler sysExHandler; - private long acceptTime; - private LcdDisplayMode acceptMode = LcdDisplayMode.NONE; - private int acceptIndex = -1; - - public enum ValueType { - SLIDER("Fader"), - KNOB("Encoder"); - private final String displayValue; - - ValueType(String value) { - this.displayValue = value; - } - - public String getDisplayValue() { - return displayValue; - } - } - - public LcdDisplay(final SysExHandler sysExHandler) { - this.sysExHandler = sysExHandler; - acceptTime = System.currentTimeMillis(); - } - - public void enableValues(final int index, final LcdDisplayMode mode) { - if (acceptMode != LcdDisplayMode.INIT) { - acceptMode = mode; - acceptIndex = index; - acceptTime = System.currentTimeMillis(); - } - } - - public void ping() { - if (acceptMode != LcdDisplayMode.NONE && (System.currentTimeMillis() - acceptTime) > 2000) { - acceptMode = LcdDisplayMode.NONE; - acceptIndex = -1; - } - } - - public void logoText(final String top, final String bottom, final KeylabIcon icon) { - final String sysex = createTopIconScreen(top, bottom, icon, true); - sysExHandler.queueTimedEvent(new TimedDelayEvent(() -> sysExHandler.sendSysexText(sysex), 100)); - } - - public void setTopIconText(final String top, final String bottom, final KeylabIcon icon, final boolean isTransient) { - final String sysex = createTopIconScreen(top, bottom, icon, isTransient); - sysExHandler.sendSysexText(sysex); - } - - private String createTopIconScreen(final String top, final String bottom, final KeylabIcon icon, - final boolean isTransient) { - final StringBuilder sysex = getScreenSysexHead("1A"); - sysex.append("01 "); - appendString(sysex, top); - sysex.append("02 "); - appendString(sysex, bottom); - sysex.append("03 "); - sysex.append(SysExHandler.toHex(icon.getKey())); - sysex.append(isTransient ? "01 " : "00 "); - sysex.append("F7"); - return sysex.toString(); - } - - public void sendPopup(final String top, final String bottom, final KeylabIcon icon) { - final String popUpTextSysEx = String.format("%s01 %s02 %s03 %02X 00 F7", getScreenSysexHead("17"), - textToSysEx(top), textToSysEx(bottom), icon.getKey()); - sysExHandler.sendSysexText(popUpTextSysEx); - } - - - public void sendLines(final String line, final boolean isTransient) { - final StringBuilder sysex = getScreenSysexHead("10"); - sysex.append("01 "); - appendString(sysex, line); - sysex.append("00 "); - sysex.append(isTransient ? "01 " : "00 "); - sysex.append("F7"); - sysExHandler.sendSysexText(sysex.toString()); - } - - private void sendLines(final String line1, final String line2, final boolean isTransient) { - final StringBuilder sysex = getScreenSysexHead("12"); - sysex.append("01 "); - appendString(sysex, line1); - sysex.append("00 "); - sysex.append("02 "); - appendString(sysex, line2); - sysex.append("00 "); - sysex.append(isTransient ? "01 " : "00 "); - sysex.append("F7"); - sysExHandler.sendSysexText(sysex.toString()); - } - - public void sendLineScroll(final String line1, final String line2, final boolean isTransient) { - final StringBuilder sysex = getScreenSysexHead("13"); - sysex.append("01 "); - appendString(sysex, line1); - sysex.append("00 "); - sysex.append("02 "); - appendString(sysex, line2); - sysex.append("00 "); - sysex.append(isTransient ? "01 " : "00 "); - sysex.append("F7"); - sysExHandler.sendSysexText(sysex.toString()); - } - - public void centerScreen() { - final StringBuilder sysex = getScreenSysexHead("1D"); - sysex.append("01 00 00 02 06 00 03 07 00 04 0D 05 00 00 05 00 F7"); - sysExHandler.sendSysexText(sysex.toString()); - } - - public void sendNavigationPage(final ContextPageConfiguration page, final boolean isTransient) { - if (page.getSecondaryText() == null) { - sendLines(page.getMainText(), isTransient); - } else { - sendLines(page.getMainText(), page.getSecondaryText(), isTransient); - } - if (page.getHeaderText() != null) { - sendHeader(page); - } - sendFooter(page); - } - - public void updateFooter(final ContextPageConfiguration page) { - sendFooter(page); - } - - public void updateHeader(final ContextPageConfiguration page) { - sendHeader(page); - } - - private void sendHeader(final ContextPageConfiguration page) { - final StringBuilder sysex = getScreenSysexHead("01"); - sysex.append("01 "); - sysex.append(SysExHandler.toHex(page.getHeaderIcon().getKey())); - sysex.append("00 "); - sysex.append("02 "); - appendString(sysex, page.getHeaderText()); - sysex.append("F7"); - sysExHandler.sendSysexText(sysex.toString()); - } - - private void sendFooter(final ContextPageConfiguration page) { - final ContextPart[] elements = page.getContextParts(); - final StringBuilder sysex = getScreenSysexHead("03"); - for (int i = 0; i < elements.length; i++) { - final int id = (i + 1) << 4; - sysex.append(SysExHandler.toHex(id)); - sysex.append(elements[i].getFrame().getHexValue()); - sysex.append("00 "); - if (!elements[i].getText().isBlank()) { - sysex.append(SysExHandler.toHex(id + 1)); - appendString(sysex, elements[i].getText()); - } - sysex.append(SysExHandler.toHex(id + 2)); - sysex.append(SysExHandler.toHex(elements[i].getIcon().getKey())); - sysex.append("00 "); - } - sysex.append("F7"); - sysExHandler.sendSysexText(sysex.toString()); - } - - public void sendValueText(final int index, final ValueType type, final LcdDisplayMode textMode, final String top, - final String bottom, final int value) { - if (textMode != acceptMode || acceptIndex != index) { - return; - } - final StringBuilder sysex = getScreenSysexHead(type == ValueType.SLIDER ? "15" : "14"); - sysex.append("01 "); - appendString(sysex, top, 12); - sysex.append("02 "); - appendString(sysex, bottom, 12); - sysex.append("03 "); - sysex.append(String.format("%02X", value)); - sysex.append("00 "); - sysex.append("01 "); - sysex.append("F7"); - sysExHandler.sendSysexText(sysex.toString()); - } - - private StringBuilder getScreenSysexHead(final String item) { - final StringBuilder sysex = new StringBuilder(SysExHandler.ARTURIA_SYSEX_HEADER); - sysex.append("04 01 "); // Command ID + Patch ID - sysex.append("60 "); // paramtype - sysex.append(item); // paramtype - sysex.append(" "); // paramtype - return sysex; - } - - public static String toSysEx(final String text) { - final StringBuilder sb = new StringBuilder(); - for (int i = 0; i < text.length(); i++) { - final char c = convert(text.charAt(i)); - final String hexValue = Integer.toHexString((byte) c); - sb.append(hexValue.length() < 2 ? "0" + hexValue : hexValue); - sb.append(" "); - } - return sb.toString(); - } - - private static void appendString(final StringBuilder sb, final String text, final int maxTextLen) { - for (int i = 0; i < text.length() && i < maxTextLen; i++) { - final char c = convert(text.charAt(i)); - final String hexValue = Integer.toHexString((byte) c); - sb.append(hexValue.length() < 2 ? "0" + hexValue : hexValue); - sb.append(" "); - } - sb.append("00 "); - } - - private static void appendString(final StringBuilder sb, final String text) { - appendString(sb, text, MAX_TEXT_LEN); - } - - private static String textToSysEx(final String text) { - final StringBuilder sb = new StringBuilder(); - for (int i = 0; i < text.length() && i < MAX_TEXT_LEN; i++) { - final char c = convert(text.charAt(i)); - final String hexValue = Integer.toHexString((byte) c); - sb.append(hexValue.length() < 2 ? "0" + hexValue : hexValue); - sb.append(" "); - } - sb.append("00 "); - return sb.toString(); - } - - private static char convert(final char c) { - if (c < 128) { - return c; - } - switch (c) { - case 'Á': - case 'À': - case 'Ä': - return 'A'; - case 'É': - case 'È': - return 'E'; - case 'á': - case 'à': - case 'ä': - return 'a'; - case 'Ö': - return 'O'; - case 'Ü': - return 'U'; - case 'è': - case 'é': - return 'e'; - case 'ö': - return 'o'; - case 'ü': - return 'u'; - case 'ß': - return 's'; - } - return '?'; - } - - + public static final int MAX_TEXT_LEN = 20; + + private final SysExHandler sysExHandler; + private long acceptTime; + private LcdDisplayMode acceptMode = LcdDisplayMode.NONE; + private int acceptIndex = -1; + + public enum ValueType { + SLIDER("Fader"), + KNOB("Encoder"); + private final String displayValue; + + ValueType(final String value) { + this.displayValue = value; + } + + public String getDisplayValue() { + return displayValue; + } + } + + public LcdDisplay(final SysExHandler sysExHandler) { + this.sysExHandler = sysExHandler; + acceptTime = System.currentTimeMillis(); + } + + public void enableValues(final int index, final LcdDisplayMode mode) { + if (acceptMode != LcdDisplayMode.INIT) { + acceptMode = mode; + acceptIndex = index; + acceptTime = System.currentTimeMillis(); + } + } + + public void ping() { + if (acceptMode != LcdDisplayMode.NONE && (System.currentTimeMillis() - acceptTime) > 2000) { + acceptMode = LcdDisplayMode.NONE; + acceptIndex = -1; + } + } + + public void logoText(final String top, final String bottom, final KeylabIcon icon) { + final String sysex = createTopIconScreen(top, bottom, icon, true); + sysExHandler.queueTimedEvent(new TimedDelayEvent(() -> sysExHandler.sendSysexText(sysex), 100)); + } + + private String createTopIconScreen(final String top, final String bottom, final KeylabIcon icon, + final boolean isTransient) { + final StringBuilder sysex = getScreenSysexHead("1A"); + sysex.append("01 "); + appendString(sysex, top); + sysex.append("02 "); + appendString(sysex, bottom); + sysex.append("03 "); + sysex.append(SysExHandler.toHex(icon.getKey())); + sysex.append(isTransient ? "01 " : "00 "); + sysex.append("F7"); + return sysex.toString(); + } + + public void sendPopup(final String top, final String bottom, final KeylabIcon icon) { + final String popUpTextSysEx = String.format( + "%s01 %s02 %s03 %02X 00 F7", getScreenSysexHead("17"), textToSysEx(top), textToSysEx(bottom), + icon.getKey()); + sysExHandler.sendSysexText(popUpTextSysEx); + } + + + public void sendLines(final String line, final boolean isTransient) { + final StringBuilder sysex = getScreenSysexHead("10"); + sysex.append("01 "); + appendString(sysex, line); + sysex.append("00 "); + sysex.append(isTransient ? "01 " : "00 "); + sysex.append("F7"); + sysExHandler.sendSysexText(sysex.toString()); + } + + private void sendLines(final String line1, final String line2, final boolean isTransient) { + final StringBuilder sysex = getScreenSysexHead("12"); + sysex.append("01 "); + appendString(sysex, line1); + sysex.append("00 "); + sysex.append("02 "); + appendString(sysex, line2); + sysex.append("00 "); + sysex.append(isTransient ? "01 " : "00 "); + sysex.append("F7"); + sysExHandler.sendSysexText(sysex.toString()); + } + + public void setTopIconText(final String top, final String bottom, final KeylabIcon icon, + final boolean isTransient) { + final String sysex = createTopIconScreen(top, bottom, icon, isTransient); + sysExHandler.sendSysexText(sysex); + } + + public void sendLineScroll(final String line1, final String line2, final boolean isTransient) { + final StringBuilder sysex = getScreenSysexHead("13"); + sysex.append("01 "); + appendString(sysex, line1); + sysex.append("00 "); + sysex.append("02 "); + appendString(sysex, line2); + sysex.append("00 "); + sysex.append(isTransient ? "01 " : "00 "); + sysex.append("F7"); + sysExHandler.sendSysexText(sysex.toString()); + } + + public void centerScreen() { + final StringBuilder sysex = getScreenSysexHead("1D"); + sysex.append("01 00 00 02 06 00 03 07 00 04 0D 05 00 00 05 00 F7"); + sysExHandler.sendSysexText(sysex.toString()); + } + + public void sendNavigationPage(final ContextPageConfiguration page, final boolean isTransient) { + if (page.getSecondaryText() == null) { + sendLines(page.getMainText(), isTransient); + } else { + sendLines(page.getMainText(), page.getSecondaryText(), isTransient); + } + if (page.getHeaderText() != null) { + sendHeader(page); + } + sendFooter(page); + } + + public void updateFooter(final ContextPageConfiguration page) { + sendFooter(page); + } + + public void updateHeader(final ContextPageConfiguration page) { + sendHeader(page); + } + + private void sendHeader(final ContextPageConfiguration page) { + final StringBuilder sysex = getScreenSysexHead("01"); + sysex.append("01 "); + sysex.append(SysExHandler.toHex(page.getHeaderIcon().getKey())); + sysex.append("00 "); + sysex.append("02 "); + appendString(sysex, page.getHeaderText()); + sysex.append("F7"); + sysExHandler.sendSysexText(sysex.toString()); + } + + private void sendFooter(final ContextPageConfiguration page) { + final ContextPart[] elements = page.getContextParts(); + final StringBuilder sysex = getScreenSysexHead("03"); + for (int i = 0; i < elements.length; i++) { + final int id = (i + 1) << 4; + sysex.append(SysExHandler.toHex(id)); + sysex.append(elements[i].getFrame().getHexValue()); + sysex.append("00 "); + if (!elements[i].getText().isBlank()) { + sysex.append(SysExHandler.toHex(id + 1)); + appendString(sysex, elements[i].getText()); + } + sysex.append(SysExHandler.toHex(id + 2)); + sysex.append(SysExHandler.toHex(elements[i].getIcon().getKey())); + sysex.append("00 "); + } + sysex.append("F7"); + sysExHandler.sendSysexText(sysex.toString()); + } + + public void sendValueText(final int index, final ValueType type, final LcdDisplayMode textMode, final String top, + final String bottom, final int value) { + if (textMode != acceptMode || acceptIndex != index) { + return; + } + final StringBuilder sysex = getScreenSysexHead(type == ValueType.SLIDER ? "15" : "14"); + sysex.append("01 "); + appendString(sysex, top, 12); + sysex.append("02 "); + appendString(sysex, bottom, 12); + sysex.append("03 "); + sysex.append(String.format("%02X", value)); + sysex.append("00 "); + sysex.append("01 "); + sysex.append("F7"); + sysExHandler.sendSysexText(sysex.toString()); + } + + private StringBuilder getScreenSysexHead(final String item) { + final StringBuilder sysex = new StringBuilder(SysExHandler.ARTURIA_SYSEX_HEADER); + sysex.append("04 01 "); // Command ID + Patch ID + sysex.append("60 "); + sysex.append(item); + sysex.append(" "); + return sysex; + } + + public static String toSysEx(final String text) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + final char c = convert(text.charAt(i)); + final String hexValue = Integer.toHexString((byte) c); + sb.append(hexValue.length() < 2 ? "0" + hexValue : hexValue); + sb.append(" "); + } + return sb.toString(); + } + + private static void appendString(final StringBuilder sb, final String text, final int maxTextLen) { + for (int i = 0; i < text.length() && i < maxTextLen; i++) { + final char c = convert(text.charAt(i)); + final String hexValue = Integer.toHexString((byte) c); + sb.append(hexValue.length() < 2 ? "0" + hexValue : hexValue); + sb.append(" "); + } + sb.append("00 "); + } + + private static void appendString(final StringBuilder sb, final String text) { + appendString(sb, text, MAX_TEXT_LEN); + } + + private static String textToSysEx(final String text) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < text.length() && i < MAX_TEXT_LEN; i++) { + final char c = convert(text.charAt(i)); + final String hexValue = Integer.toHexString((byte) c); + sb.append(hexValue.length() < 2 ? "0" + hexValue : hexValue); + sb.append(" "); + } + sb.append("00 "); + return sb.toString(); + } + + private static char convert(final char c) { + if (c < 128) { + return c; + } + return switch (c) { + case 'Á', 'À', 'Ä' -> 'A'; + case 'É', 'È' -> 'E'; + case 'á', 'à', 'ä' -> 'a'; + case 'Ö' -> 'O'; + case 'Ü' -> 'U'; + case 'è', 'é' -> 'e'; + case 'ö' -> 'o'; + case 'ü' -> 'u'; + case 'ß' -> 's'; + default -> '?'; + }; + } + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/MainScreenSection.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/MainScreenSection.java index 469c626b..62f64045 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/MainScreenSection.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/MainScreenSection.java @@ -50,7 +50,7 @@ public MainScreenSection(final HwElements hwElements, final Layers layers) { navigationLayer = new Layer(layers, "SCREEN NAVIGATION"); } - ///KeylabIcon selector = KeylabIcon.NONE; + /// KeylabIcon selector = KeylabIcon.NONE; @PostConstruct void init() { @@ -84,7 +84,8 @@ private void initNavigationScreen() { ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); - context3.bindRepeatHold(navigationLayer, cursorTrack::selectPrevious, BUTTON_WAIT_UTIL_REPEAT, + context3.bindRepeatHold( + navigationLayer, cursorTrack::selectPrevious, BUTTON_WAIT_UTIL_REPEAT, HOLD_REPEAT_FREQUENCY); context3.bindLight(navigationLayer, () -> canScroll(cursorTrack.hasPrevious())); @@ -122,17 +123,12 @@ public void notifyPadMode(final SysExHandler.PadMode padMode) { } public RgbButton getContextButton(final int which) { - switch (which) { - case 0: - return context1; - case 1: - return context2; - case 2: - return context3; - case 3: - return context4; - } - return context1; + return switch (which) { + case 1 -> context2; + case 2 -> context3; + case 3 -> context4; + default -> context1; // 0 too + }; } public Layer getLayer() { diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/MainViewDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/MainViewDisplayBinding.java index 443bbbe7..890f4dc5 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/MainViewDisplayBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/display/MainViewDisplayBinding.java @@ -4,43 +4,44 @@ import com.bitwig.extensions.framework.Binding; public class MainViewDisplayBinding extends Binding { - - private final LcdDisplay display; - - public MainViewDisplayBinding(ContextPageConfiguration target, LcdDisplay display, StringValue... source) { - super(target, source, target); - if (source.length < 2) { - throw new IllegalArgumentException("At least 2 sources needed"); - } - this.display = display; - source[0].addValueObserver(this::handleValueChanged1); - source[1].addValueObserver(this::handleValueChanged2); - } - - private void handleValueChanged1(String value) { - if (!isActive()) { - return; - } - getTarget().setMainText(value); - display.sendNavigationPage(getTarget(), false); - } - - private void handleValueChanged2(String value) { - if (!isActive()) { - return; - } - getTarget().setSecondaryText(value); - display.sendNavigationPage(getTarget(), false); - } - - @Override - protected void deactivate() { - } - - @Override - protected void activate() { - getTarget().setMainText(getSource()[0].get()); - getTarget().setSecondaryText(getSource()[1].get()); - display.sendNavigationPage(getTarget(), false); - } + + private final LcdDisplay display; + + public MainViewDisplayBinding(final ContextPageConfiguration target, final LcdDisplay display, + final StringValue... source) { + super(target, source, target); + if (source.length < 2) { + throw new IllegalArgumentException("At least 2 sources needed"); + } + this.display = display; + source[0].addValueObserver(this::handleValueChanged1); + source[1].addValueObserver(this::handleValueChanged2); + } + + private void handleValueChanged1(final String value) { + if (!isActive()) { + return; + } + getTarget().setMainText(value); + display.sendNavigationPage(getTarget(), false); + } + + private void handleValueChanged2(final String value) { + if (!isActive()) { + return; + } + getTarget().setSecondaryText(value); + display.sendNavigationPage(getTarget(), false); + } + + @Override + protected void deactivate() { + } + + @Override + protected void activate() { + getTarget().setMainText(getSource()[0].get()); + getTarget().setSecondaryText(getSource()[1].get()); + display.sendNavigationPage(getTarget(), false); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/KeylabMk3ControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/KeylabMk3ControllerExtension.java index 620df3ff..02f7e2c8 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/KeylabMk3ControllerExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/KeylabMk3ControllerExtension.java @@ -3,8 +3,6 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import com.bitwig.extension.api.PlatformType; -import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; import com.bitwig.extension.controller.ControllerExtension; import com.bitwig.extension.controller.api.Action; import com.bitwig.extension.controller.api.Application; @@ -29,6 +27,7 @@ public class KeylabMk3ControllerExtension extends ControllerExtension { private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); private static ControllerHost debugHost; + private Layer mainLayer; private HardwareSurface surface; private MidiProcessor midiProcessor; @@ -57,8 +56,6 @@ public void init() { mainLayer = diContext.createLayer("MAIN_LAYER"); midiProcessor = new MidiProcessor(host); diContext.registerService(MidiProcessor.class, midiProcessor); - final AutoDetectionMidiPortNamesList ports = - getExtensionDefinition().getAutoDetectionMidiPortNamesList(PlatformType.MAC); final ClipLaunchingLayer clipLauncher = diContext.getService(ClipLaunchingLayer.class); setUpPreferences(clipLauncher); @@ -76,14 +73,16 @@ public void init() { private void setUpPreferences(final ClipLaunchingLayer clipLauncher) { final DocumentState documentState = getHost().getDocumentState(); - final SettableEnumValue recordButtonAssignment = documentState.getEnumSetting("Record Button assignment", + final SettableEnumValue recordButtonAssignment = documentState.getEnumSetting( + "Record Button assignment", // "Transport", new String[] {FocusMode.LAUNCHER.getDescriptor(), FocusMode.ARRANGER.getDescriptor()}, recordFocusMode.getDescriptor()); recordButtonAssignment.addValueObserver(value -> recordFocusMode = FocusMode.toMode(value)); final Preferences preferences = getHost().getPreferences(); - final SettableEnumValue clipStopTiming = preferences.getEnumSetting("Long press to stop clip", // + final SettableEnumValue clipStopTiming = preferences.getEnumSetting( + "Long press to stop clip", // "Clip", new String[] {"Fast", "Medium", "Standard"}, "Medium"); clipStopTiming.addValueObserver(clipLauncher::setClipStopTiming); } @@ -106,7 +105,8 @@ private void bindTransport(final Context context) { application.panelLayout().addValueObserver(layout -> this.panelLayout = LayoutType.toType(layout)); final RgbButton playButton = hwElements.getButton(CcAssignment.PLAY); playButton.bindPressed(mainLayer, transport::play); - playButton.bindLight(mainLayer, + playButton.bindLight( + mainLayer, () -> transport.isPlaying().get() ? RgbLightState.GREEN : RgbLightState.GREEN_DIMMED); final RgbButton recordButton = hwElements.getButton(CcAssignment.RECORD); @@ -114,47 +114,49 @@ private void bindTransport(final Context context) { recordButton.bindLight(mainLayer, () -> getRecordingLightState(transport)); final RgbButton stopButton = hwElements.getButton(CcAssignment.STOP); - stopButton.bindPressed(mainLayer, () -> transport.stop()); + stopButton.bindPressed(mainLayer, transport::stop); stopButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); final RgbButton fastForwardButton = hwElements.getButton(CcAssignment.FAST_FWD); final RgbButton rewindButton = hwElements.getButton(CcAssignment.REWIND); - fastForwardButton.bindRepeatHold(mainLayer, () -> transport.fastForward(), 400, 100); + fastForwardButton.bindRepeatHold(mainLayer, transport::fastForward, 400, 100); fastForwardButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); - rewindButton.bindRepeatHold(mainLayer, () -> transport.rewind(), 400, 100); + rewindButton.bindRepeatHold(mainLayer, transport::rewind, 400, 100); rewindButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); final RgbButton loopButton = hwElements.getButton(CcAssignment.LOOP); - loopButton.bindToggle(mainLayer, transport.isArrangerLoopEnabled(), RgbLightState.ORANGE, + loopButton.bindToggle( + mainLayer, transport.isArrangerLoopEnabled(), RgbLightState.ORANGE, RgbLightState.ORANGE_DIMMED); final RgbButton tapButton = hwElements.getButton(CcAssignment.TAP); tapButton.bind(mainLayer, transport.tapTempoAction(), RgbLightState.WHITE, RgbLightState.WHITE_DIMMED); final RgbButton metroButton = hwElements.getButton(CcAssignment.METRO); - metroButton.bindToggle(mainLayer, transport.isMetronomeEnabled(), RgbLightState.WHITE, + metroButton.bindToggle( + mainLayer, transport.isMetronomeEnabled(), RgbLightState.WHITE, RgbLightState.WHITE_DIMMED); application.canUndo().markInterested(); application.canRedo().markInterested(); final RgbButton undoButton = hwElements.getButton(CcAssignment.UNDO); undoButton.bindPressed(mainLayer, application::undo); - undoButton.bindLight(mainLayer, + undoButton.bindLight( + mainLayer, () -> application.canUndo().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); final RgbButton redoButton = hwElements.getButton(CcAssignment.REDO); redoButton.bindPressed(mainLayer, application::redo); - redoButton.bindLight(mainLayer, + redoButton.bindLight( + mainLayer, () -> application.canRedo().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); final RgbButton quantizeButton = hwElements.getButton(CcAssignment.QUANTIZE); quantizeButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); - quantizeButton.bindPressed(mainLayer, () -> invokeQuantize()); + quantizeButton.bindPressed(mainLayer, this::invokeQuantize); final RgbButton saveButton = hwElements.getButton(CcAssignment.SAVE); final Action saveAction = application.getAction("Save"); saveButton.bindLight(mainLayer, RgbLightState.WHITE_DIMMED, RgbLightState.WHITE); - saveButton.bindPressed(mainLayer, () -> { - saveAction.invoke(); - }); + saveButton.bindPressed(mainLayer, saveAction::invoke); } private void invokeQuantize() { @@ -162,20 +164,24 @@ private void invokeQuantize() { final Clip clip = viewControl.getArrangerClip(); if (clip.exists().get()) { viewControl.invokeArrangerQuantize(); - midiProcessor.screenLine2(ScreenTarget.POP_SCREEN_2_LINES, "Arrangement", RgbColor.AQUA, + midiProcessor.screenLine2( + ScreenTarget.POP_SCREEN_2_LINES, "Arrangement", RgbColor.AQUA, "Clip Quantized", RgbColor.WHITE, null); } else { - midiProcessor.screenLine2(ScreenTarget.POP_SCREEN_2_LINES, "Arrangement", RgbColor.AQUA, + midiProcessor.screenLine2( + ScreenTarget.POP_SCREEN_2_LINES, "Arrangement", RgbColor.AQUA, "Quantization: No Clip", RgbColor.WHITE, null); } } else { final Clip clip = viewControl.getCursorClip(); if (clip.exists().get()) { viewControl.invokeLauncherQuantize(); - midiProcessor.screenLine2(ScreenTarget.POP_SCREEN_2_LINES, "Launcher", RgbColor.AQUA, "Clip Quantized", + midiProcessor.screenLine2( + ScreenTarget.POP_SCREEN_2_LINES, "Launcher", RgbColor.AQUA, "Clip Quantized", RgbColor.WHITE, null); } else { - midiProcessor.screenLine2(ScreenTarget.POP_SCREEN_2_LINES, "Launcher", RgbColor.AQUA, + midiProcessor.screenLine2( + ScreenTarget.POP_SCREEN_2_LINES, "Launcher", RgbColor.AQUA, "Quantization: No Clip", RgbColor.WHITE, null); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/BankControl.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/BankControl.java index 6b935e6b..6f9bb174 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/BankControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/BankControl.java @@ -7,6 +7,7 @@ import com.bitwig.extension.controller.api.DeviceBank; import com.bitwig.extension.controller.api.PinnableCursorDevice; import com.bitwig.extensions.controllers.nativeinstruments.komplete.midi.MidiProcessor; +import com.bitwig.extensions.framework.values.BooleanValueObject; import com.bitwig.extensions.framework.values.ValueObject; public class BankControl { @@ -27,6 +28,7 @@ public class BankControl { private final ValueObject currentFocus = new ValueObject<>(Focus.DEVICE); private boolean trackRemotesPresent; private boolean projectRemotesPresent; + private final BooleanValueObject shiftHeld; private static class ParentTab implements DeviceSelectionTab { @Override @@ -42,11 +44,12 @@ public enum Focus { } public BankControl(final PinnableCursorDevice cursorDevice, final MidiProcessor midiProcessor, - final DeviceControl deviceControl) { + final DeviceControl deviceControl, final BooleanValueObject shiftHeld) { final DeviceBank deviceBank = cursorDevice.deviceChain().createDeviceBank(64); this.midiProcessor = midiProcessor; this.deviceControl = deviceControl; this.cursorDevice = cursorDevice; + this.shiftHeld = shiftHeld; this.parentNavTab = new ParentTab(); cursorDevice.position().addValueObserver(this::handleCursorDevicePosition); this.trackDevice = new DeviceSlot(-1, "Track"); @@ -59,6 +62,7 @@ public BankControl(final PinnableCursorDevice cursorDevice, final MidiProcessor device.exists().addValueObserver(exists -> handleExistChange(slot, exists)); device.hasLayers().addValueObserver(hasLayers -> handleHasLayers(slot, hasLayers)); device.hasDrumPads().addValueObserver(hasPads -> handleHasDrumPads(slot, hasPads)); + device.isEnabled().addValueObserver(enabled -> handleEnabled(slot, enabled)); } cursorDevice.exists().markInterested(); cursorDevice.isNested().addValueObserver(this::handleNested); @@ -139,6 +143,12 @@ private void handleHasDrumPads(final DeviceSlot slot, final boolean hasPads) { deviceControl.triggerUpdateAction(); } + + private void handleEnabled(final DeviceSlot slot, final boolean enabled) { + slot.setEnabled(enabled); + deviceControl.triggerUpdateAction(); + } + private void handleHasLayers(final DeviceSlot slot, final boolean hasLayers) { slot.setHasLayers(hasLayers); deviceControl.triggerUpdateAction(); @@ -212,24 +222,33 @@ public void select(final int... selectionPath) { index -= getIndexOffset(); } currentFocus.set(Focus.DEVICE); - final DeviceSlot slot = devices.get(index); + + if (selectionPath.length == 1) { - slot.select(); - if (slot.isSelected()) { - if (slot.hasLayers()) { - slot.toggleExpanded(); + if (shiftHeld.get()) { + slot.toggleEnabled(); + } else { + slot.select(); + if (slot.isSelected()) { + if (slot.hasLayers()) { + slot.toggleExpanded(); + } + deviceControl.triggerUpdateAction(); + } else { + devices.forEach(d -> d.setSelected(false)); + //devices.forEach(d -> d.setExpanded(false)); + slot.setSelected(true); + deviceControl.triggerUpdateAction(); } + } + } else { + if (shiftHeld.get()) { + slot.toggleLayerActive(selectionPath[1]); deviceControl.triggerUpdateAction(); } else { - devices.forEach(d -> d.setSelected(false)); - //devices.forEach(d -> d.setExpanded(false)); - slot.setSelected(true); - deviceControl.triggerUpdateAction(); + cursorDevice.selectDevice(slot.selectLayer(selectionPath[1])); } - } else { - final Device device = slot.selectLayer(selectionPath[1]); - cursorDevice.selectDevice(device); } } } diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/DeviceControl.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/DeviceControl.java index d3a19fa5..8878bacf 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/DeviceControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/DeviceControl.java @@ -55,7 +55,7 @@ public DeviceControl(final ControllerHost host, final MidiProcessor midiProcesso this.controlElements = controlElements; final PinnableCursorDevice cursorDevice = cursorTrack.createCursorDevice(); cursorDevice.presetName().addValueObserver(this::handlePresetName); - this.mainBank = new BankControl(cursorDevice, this.midiProcessor, this); + this.mainBank = new BankControl(cursorDevice, this.midiProcessor, this, controlElements.getShiftHeld()); this.mainBank.getCurrentFocus().addValueObserver(this::handleFocus); final BrowserHandler browserHandler = new BrowserHandler(host, cursorDevice, controlElements.getShiftHeld()); @@ -63,8 +63,7 @@ public DeviceControl(final ControllerHost host, final MidiProcessor midiProcesso deviceRemotesControl = new RemotesControl(deviceRemoteLayer, deviceRemotePages, controlElements, midiProcessor); directParameterControl = new DirectParameterControl( - directParamLayer, cursorDevice, controlElements, midiProcessor, - deviceRemotePages.pageCount()); + directParamLayer, cursorDevice, controlElements, midiProcessor, deviceRemotePages.pageCount()); directParameterControl.getDirectActive().addValueObserver(this::handleDirectActive); final Track rootTrack = viewControl.getProject().getRootTrackGroup(); diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/DeviceSlot.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/DeviceSlot.java index 4910f3be..8b5f2368 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/DeviceSlot.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/DeviceSlot.java @@ -15,6 +15,7 @@ public class DeviceSlot implements DeviceSelectionTab { private boolean hasDrumPads; private boolean isExpanded; + private boolean enabled; private final Device device; private boolean selected; private final List layerSlots = new ArrayList<>(); @@ -38,7 +39,7 @@ public DeviceSlot(final int index, final String name) { } public List getLayerList() { - return layerSlots.stream().filter(LayerSlot::isExists).map(LayerSlot::getName).toList(); + return layerSlots.stream().filter(LayerSlot::isExists).map(LayerSlot::displayName).toList(); } public String getName() { @@ -83,17 +84,32 @@ public void setSelected(final boolean selected) { this.selected = selected; } + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + private String displayName() { + if (enabled) { + return name; + } + return "[x] %s".formatted(name); + } + @Override public String getLayerCode() { if (!hasLayers && !hasDrumPads) { - return name; + return displayName(); } if (isExpanded) { return "{\"n\":\"%s\",\"c\":[%s]}".formatted( - name, + displayName(), getLayerList().stream().map("\"%s\""::formatted).collect(Collectors.joining(","))); } else { - return "{\"n\":\"%s\",\"c\":[]}".formatted(name); + return "{\"n\":\"%s\",\"c\":[]}".formatted(displayName()); } } @@ -112,4 +128,12 @@ public Device selectLayer(final int slotIndex) { layer.select(); return layer.getDevice(); } + + public void toggleLayerActive(final int slotIndex) { + layerSlots.get(slotIndex).toggleActive(); + } + + public void toggleEnabled() { + device.isEnabled().toggle(); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/LayerSlot.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/LayerSlot.java index 4667cb2b..eea321ed 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/LayerSlot.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/device/LayerSlot.java @@ -10,6 +10,7 @@ class LayerSlot { private final int index; private boolean exists; private String name; + private boolean active; public LayerSlot(final int index, final DeviceLayer layer) { this.layer = layer; @@ -18,6 +19,19 @@ public LayerSlot(final int index, final DeviceLayer layer) { this.device = bank.getDevice(0); this.layer.exists().addValueObserver(this::handleExists); this.layer.name().addValueObserver(this::handleName); + this.layer.isActivated().addValueObserver(this::handleActivated); + } + + private void handleActivated(final boolean activated) { + this.active = activated; + } + + public boolean isActive() { + return active; + } + + public void toggleActive() { + this.layer.isActivated().toggle(); } public void select() { @@ -56,4 +70,8 @@ public void setName(final String name) { this.name = name; } + public String displayName() { + return active ? name : "[x] %s".formatted(name); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropColor.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropColor.java new file mode 100644 index 00000000..9d1937f3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropColor.java @@ -0,0 +1,178 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments; + +import java.util.List; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; + +public class DropColor extends InternalHardwareLightState { + + private final int colorIndex; + + private final static int MASK_PLAYING = 0x40; + private final static int MASK_RECORDING = 0x20; + private final static int MASK_TRIGGERED = 0x10; + + private static final List COLOR_SEQ = List.of( // + new ColorMatch(0, 0, 0, 0), // + new ColorMatch(1, 0, 0, 0), // 0x1 Black + new ColorMatch(2, 255, 0, 0), // 0x2 RED + new ColorMatch(3, 255, 165, 0), // 0x3 ORANGE + new ColorMatch(4, 165, 42, 42), // 0x4 BROWN + new ColorMatch(5, 55, 255, 0), // 0x5 YELLOW + new ColorMatch(6, 44, 238, 144), // 0x6 Light GREEN + new ColorMatch(7, 0, 255, 0), // 0x7 GREEN + new ColorMatch(8, 244, 255, 255), // 0x8 Light Cyan + new ColorMatch(9, 0, 255, 255), // 0x9 Cyan + new ColorMatch(10, 173, 216, 230), // 0xA Light blue + new ColorMatch(11, 0, 0, 255), // 0xB Blue + new ColorMatch(12, 255, 0, 255), // 0xC Magenta + new ColorMatch(13, 255, 182, 193), // 0xD Light Pink + new ColorMatch(14, 255, 192, 203), // 0xE Pink + new ColorMatch(15, 255, 255, 255), // 0xF WHITE + // + new ColorMatch(5, 255, 255, 38), // 0x5 YELLOW + new ColorMatch(10, 134, 137, 172), // 0xA LIGHT BLUE + new ColorMatch(11, 87, 97, 198), // 0xB LIGHT BLUE + new ColorMatch(12, 149, 73, 203), // 0xC MAGENTA + new ColorMatch(5, 246, 246, 156), // 0x5 YELLOW + new ColorMatch(5, 219, 188, 28), // 0x5 YELLOW + new ColorMatch(4, 244, 168, 147), // 0x4 BROWN + new ColorMatch(4, 163, 121, 67), // 0x4 BROWN + new ColorMatch(7, 0, 157, 71), // 0x7 GREEN + new ColorMatch(7, 0, 166, 148), // 0x7 GREEN + new ColorMatch(7, 67, 210, 185), // 0x7 GREEN + new ColorMatch(2, 217, 46, 36), // 0x2 RED + new ColorMatch(2, 172, 35, 59), // 0x2 RED + new ColorMatch(2, 236, 97, 87), // 0x2 RED + new ColorMatch(3, 255, 87, 6), // 0x3 ORANGE + new ColorMatch(3, 236, 97, 87), // 0x3 ORANGE + new ColorMatch(3, 255, 131, 62), // 0x3 ORANGE + new ColorMatch(11, 134, 137, 172), // 0xE Pink + new ColorMatch(6, 115, 152, 20), // 0x6 Light GREEN + new ColorMatch(9, 68, 200, 255), // 0x9 Cyan + new ColorMatch(9, 0, 153, 217), // 0x9 Cyan + new ColorMatch(6, 160, 192, 76), // 0x6 Light GREEN + new ColorMatch(6, 139, 232, 184), // 0x6 Light GREEN + new ColorMatch(10, 88, 180, 186), // 0xA Light blue + new ColorMatch(12, 78, 0, 137), // 0xC Magenta + new ColorMatch(14, 225, 102, 145), // PINK + new ColorMatch(10, 90, 177, 245), // Light BLUE + new ColorMatch(10, 9, 156, 183), // Light BLUE + new ColorMatch(11, 90, 177, 245), // BLUE + new ColorMatch(11, 11, 115, 194), // BLUE + new ColorMatch(11, 14, 142, 240), // BLUE + // + new ColorMatch(13, 209, 185, 216) // BW PINK + ); + + public static final DropColor OFF = new DropColor(1); + public static final DropColor RED = new DropColor(2); + public static final DropColor GREEN = new DropColor(7); + public static final DropColor BLUE = new DropColor(11); + public static final DropColor WHITE = new DropColor(0xF); + + + private record ColorMatch(int index, int red, int green, int blue, Color color) { + + public ColorMatch(final int index, final int red, final int green, final int blue) { + this(index, red, green, blue, Color.fromRGB255(red, green, blue)); + } + + private double colorDistance(final double r1, final double g1, final double b1) { + final double dr = r1 - (this.red / 255.0); + final double dg = g1 - (this.green / 255.0); + final double db = b1 - (this.blue / 255.0); + + return Math.sqrt(dr * dr + dg * dg + db * db); // Euclidean distance + } + } + + private final static DropColor[] colors = new DropColor[128]; + + static { + for (int i = 0; i < 128; i++) { + colors[i] = new DropColor(i); + } + } + + private DropColor(final int colorIndex) { + this.colorIndex = colorIndex; + } + + public static DropColor fromRgb(final double r, final double g, final double b) { + final int index = rgbToIndex(r, g, b); + return colors[index]; + } + + public static DropColor fromRgb(final double r, final double g, final double b, final DropColor defaultColor) { + if (r == g && g == b) { + if ((int) Math.round(r * 255) == 80) { + return defaultColor; + } + } + return colors[rgbToIndex(r, g, b)]; + } + + public DropColor playing() { + return colors[this.colorIndex & 0xF | MASK_PLAYING]; + } + + public DropColor recording() { + return colors[this.colorIndex & 0xF | MASK_RECORDING]; + } + + public DropColor triggered() { + return colors[this.colorIndex & 0xF | MASK_TRIGGERED]; + } + + @Override + public HardwareLightVisualState getVisualState() { + if (colorIndex == 0) { + return null; + } + return HardwareLightVisualState.createForColor(COLOR_SEQ.get(colorIndex & 0xF).color()); + } + + @Override + public boolean equals(final Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + final DropColor dropColor = (DropColor) o; + return colorIndex == dropColor.colorIndex; + } + + public int getColorIndex() { + return colorIndex; + } + + @Override + public int hashCode() { + return colorIndex; + } + + public static int rgbToIndex(final double red, final double green, final double blue) { + int closestIndex = -1; + double minDistance = Double.MAX_VALUE; + + if (red == green && green == blue) { + return 15; + } + + for (int i = 1; i < COLOR_SEQ.size(); i++) { + final ColorMatch c = COLOR_SEQ.get(i); + final double distance = c.colorDistance(red, green, blue); + + if (distance < minDistance) { + minDistance = distance; + closestIndex = i; + } + } + + return COLOR_SEQ.get(closestIndex).index(); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropExtension.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropExtension.java new file mode 100644 index 00000000..d5c94b49 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropExtension.java @@ -0,0 +1,49 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import com.bitwig.extension.controller.ControllerExtension; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extensions.framework.di.Context; + +public class DropExtension extends ControllerExtension { + private static ControllerHost debugHost; + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + + private HardwareSurface surface; + private final DropExtensionDefinition definition; + private Context diContext; + + public static void println(final String format, final Object... args) { + if (debugHost != null) { + final LocalDateTime now = LocalDateTime.now(); + debugHost.println(now.format(DF) + " > " + String.format(format, args)); + } + } + + protected DropExtension(final DropExtensionDefinition definition, final ControllerHost host) { + super(definition, host); + this.definition = definition; + } + + @Override + public void init() { + debugHost = getHost(); + diContext = new Context(this); + surface = diContext.getService(HardwareSurface.class); + diContext.activate(); + } + + @Override + public void exit() { + diContext.deactivate(); + } + + @Override + public void flush() { + surface.updateHardware(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropExtensionDefinition.java new file mode 100644 index 00000000..1b760ddf --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropExtensionDefinition.java @@ -0,0 +1,88 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.ControllerExtensionDefinition; +import com.bitwig.extension.controller.api.ControllerHost; + +public class DropExtensionDefinition extends ControllerExtensionDefinition { + + private static final UUID DRIVER_ID = UUID.fromString("a817b682-a2ae-4ea6-8ec0-a53acae289df"); + + public DropExtensionDefinition() { + } + + @Override + public String getName() { + return "Drop"; + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "1.00"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareVendor() { + return "Neuzeit Instruments"; + } + + @Override + public String getHardwareModel() { + return "Drop"; + } + + @Override + public int getRequiredAPIVersion() { + return 24; + } + + @Override + public int getNumMidiInPorts() { + return 1; + } + + @Override + public int getNumMidiOutPorts() { + return 1; + } + + @Override + public boolean isUsingBetaAPI() { + return false; + } + + @Override + public String getHelpFilePath() { + return "Controllers/Neuzeit Instruments/Neuzeit Instruments Drop.pdf"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS || platformType == PlatformType.MAC) { + list.add(new String[] {"DROP USB1"}, new String[] {"DROP USB1"}); + } else { + list.add(new String[] {"DROP USB1 MIDI1"}, new String[] {"DROP USB1 MIDI1"}); + } + } + + @Override + public DropExtension createInstance(final ControllerHost host) { + return new DropExtension(this, host); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropMidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropMidiProcessor.java new file mode 100644 index 00000000..a6921e28 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropMidiProcessor.java @@ -0,0 +1,133 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.IntConsumer; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.time.TimedEvent; +import com.bitwig.extensions.framework.values.Midi; + +@Component +public class DropMidiProcessor { + + private final ControllerHost host; + private final MidiIn midiIn; + private final MidiOut midiOut; + private final List dropRequestListeners = new ArrayList<>(); + protected final Queue timedEvents = new ConcurrentLinkedQueue<>(); + + public DropMidiProcessor(final ControllerHost host) { + this.host = host; + this.midiIn = host.getMidiInPort(0); + this.midiOut = host.getMidiOutPort(0); + midiIn.setMidiCallback(this::handleMidiIn); + midiIn.setSysexCallback(this::handleSysEx); + + setUpKeyboardChannel(host); + host.scheduleTask(this::processMidi, 200); + } + + private void setUpKeyboardChannel(final ControllerHost host) { + final String[] filters = Stream.of("1") // + .map(index -> getKeyboardFilter(index, "1"))// + .flatMap(Optional::stream) // + .distinct() // + .toArray(String[]::new); + if (filters.length > 0) { + midiIn.createNoteInput("KEYBOARD", filters); + } + } + + private Optional getKeyboardFilter(final String index, final String defaultValue) { + final SettableEnumValue preferenceValue = this.host.getPreferences().getEnumSetting( + "Keyboard Channel", "Notes", Stream.concat( + Stream.of("Off"), IntStream.rangeClosed(1, 15) // + .filter(v -> v != 2) // + .mapToObj(String::valueOf)) // convert int → String + .toArray(String[]::new), defaultValue); + preferenceValue.markInterested(); + final String value = preferenceValue.get(); + if (value.equals("Off")) { + return Optional.empty(); + } + final int channel = Integer.parseInt(preferenceValue.get()); + final String filter = "9%X????".formatted(channel - 1); + return Optional.of(filter); + } + + + public void assignNoteAction(final HardwareButton hwButton, final int midiNote) { + hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(0xF, midiNote)); + hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(0xF, midiNote)); + } + + public void assignCcAction(final HardwareButton hwButton, final int channel, final int ccNr) { + hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccNr, 0x7F)); + hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccNr, 0x0)); + } + + public void assignCcMatcher(final AbsoluteHardwareControl control, final int channel, final int ccValue) { + control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(channel, ccValue)); + } + + public void queueEvent(final TimedEvent event) { + timedEvents.add(event); + } + + private void processMidi() { + if (!timedEvents.isEmpty()) { + for (final TimedEvent event : timedEvents) { + event.process(); + if (event.isCompleted()) { + timedEvents.remove(event); + } + } + } + host.scheduleTask(this::processMidi, 50); + } + + + public void addDropRequestListener(final IntConsumer dropRequestListener) { + dropRequestListeners.add(dropRequestListener); + } + + private void handleSysEx(final String data) { + DropExtension.println(" SYS_EX = %s", data); + } + + private void handleMidiIn(final int status, final int data1, final int data2) { + if (status == 0x9F && data1 == 0x53) { + dropRequestListeners.forEach(listener -> listener.accept(data2)); + } else { + DropExtension.println("MIDI => %02X %02X %02X %d", status, data1, data2, data1); + } + } + + public MidiIn getMidiIn() { + return midiIn; + } + + public void sendMidi(final int status, final int val1, final int val2) { + midiOut.sendMidi(status, val1, val2); + } + + public void sendMidiDelayed(final int status, final int val1, final int val2) { + host.scheduleTask(() -> midiOut.sendMidi(status, val1, val2), 500); + } + + public void setLayoutMode(final int mode) { + midiOut.sendMidi(Midi.NOTE_ON | 0xF, 83, mode); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropViewControl.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropViewControl.java new file mode 100644 index 00000000..cf0c282a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/DropViewControl.java @@ -0,0 +1,74 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.view.OverviewGrid; + +@Component +public class DropViewControl { + private final Track rootTrack; + private final CursorTrack cursorTrack; + private final TrackBank launcherTrackBank; + private final TrackBank arrangerTrackBank; + private final OverviewGrid overviewGrid; + private final TrackBank maxTrackBank; + + public DropViewControl(final ControllerHost host) { + rootTrack = host.getProject().getRootTrackGroup(); + launcherTrackBank = host.createTrackBank(4, 1, 4, true); + arrangerTrackBank = host.createTrackBank(5, 1, 3, true); + cursorTrack = host.createCursorTrack(1, 4); + maxTrackBank = host.createTrackBank(64, 1, 64, false); + overviewGrid = new OverviewGrid(maxTrackBank); + prepareBank(launcherTrackBank); + prepareBank(arrangerTrackBank); + overviewGrid.setUpFocusScene(launcherTrackBank); + rootTrack.isQueuedForStop().markInterested(); + } + + private void prepareBank(final TrackBank bank) { + for (int i = 0; i < bank.getSizeOfBank(); i++) { + prepareTrack(bank.getItemAt(i)); + } + } + + public TrackBank getLauncherTrackBank() { + return launcherTrackBank; + } + + public TrackBank getArrangerTrackBank() { + return arrangerTrackBank; + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public Track getRootTrack() { + return rootTrack; + } + + public boolean hasQueuedClips(final int sceneIndex) { + return overviewGrid.hasQueuedScenes(sceneIndex); + } + + public boolean hasPlayingClips(final int sceneIndex) { + return overviewGrid.hasPlayingClips(sceneIndex); + } + + private void prepareTrack(final Track track) { + track.arm().markInterested(); + track.exists().markInterested(); + track.solo().markInterested(); + track.mute().markInterested(); + track.isGroup().markInterested(); + track.isGroupExpanded().markInterested(); + track.isMutedBySolo().markInterested(); + track.isQueuedForStop().markInterested(); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/HwElements.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/HwElements.java new file mode 100644 index 00000000..656c4693 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/HwElements.java @@ -0,0 +1,143 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments; + +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extensions.controllers.neuzeitinstruments.controls.DropButton; +import com.bitwig.extensions.controllers.neuzeitinstruments.controls.DropColorButton; +import com.bitwig.extensions.controllers.neuzeitinstruments.controls.DropRemoteMapControl; +import com.bitwig.extensions.controllers.neuzeitinstruments.controls.MapButton; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.values.MidiStatus; + +@Component +public class HwElements { + + public static final String PREF_BUTTON = "Buttons"; + public static final String PREF_CONTROLLER = "Controller"; + private final List layer1Buttons = new ArrayList<>(); + private final List layer2Buttons = new ArrayList<>(); + private final DropButton gridLeftButton; + private final DropButton gridRightButton; + private final DropButton gridUpButton; + private final DropButton gridDownButton; + private final List mappingKnobs = new ArrayList<>(); + private final List mappingButtons = new ArrayList<>(); + + private record DawControlInfo(String name, int offset, int channel, Integer jumpIndex) { + public DawControlInfo(final String name, final int offset) { + this(name, offset, 0, null); + } + + public DawControlInfo(final String name, final int offset, final Integer jumpIndex) { + this(name, offset, 0, jumpIndex); + } + + public int getValue(final int index) { + if (jumpIndex != null) { + final int pos = jumpIndex & 0xF; + final int amount = jumpIndex >> 4; + if (index >= pos) { + return offset + index + amount; + } + } + return offset + index; + } + } + + private static final List DAW_KNOBS = List.of( + new DawControlInfo("FADER A", 0x2C), // + new DawControlInfo("ROT A-1", 0x01, 0x15), // + new DawControlInfo("ROT A-2", 0x0A), // + new DawControlInfo("ROT A-3", 0x12), // + new DawControlInfo("ROT A-4", 0x1A, 0x16), // + new DawControlInfo("FADER B", 0x5C, 0x25), // +2 + new DawControlInfo("ROT B-1", 0x34), // + new DawControlInfo("ROT B-2", 0x3c), // + new DawControlInfo("ROT B-3", 0x44), // + new DawControlInfo("ROT B-4", 0x4C)); + + private static final List DAW_PUSH_ENCODERS = List.of( + new DawControlInfo("PUSH A-1", 0x01, 0x15), // + new DawControlInfo("PUSH A-2", 0x0A), // + new DawControlInfo("PUSH A-3", 0x12), // + new DawControlInfo("PUSH A-4", 0x1A, 0x16), // + new DawControlInfo("PUSH B-1", 0x34), // + new DawControlInfo("PUSH B-2", 0x3c), // + new DawControlInfo("PUSH B-3", 0x44), // + new DawControlInfo("PUSH B-4", 0x4C)); + + private static final List DAW_BUTTONS = List.of( + new DawControlInfo("BTN A", 0x23, 0x13), // + new DawControlInfo("BTN B", 0x54) // + ); + + public HwElements(final HardwareSurface surface, final DropMidiProcessor midiProcessor, final ControllerHost host) { + for (int i = 0; i < 20; i++) { + layer1Buttons.add(new DropColorButton(i, 88 + i, "L1BUTTON", surface, midiProcessor)); + layer2Buttons.add(new DropColorButton(i, 108 + i, "L2BUTTON", surface, midiProcessor)); + } + + this.gridLeftButton = new DropButton(0, 84, "NAV_LEFT", surface, midiProcessor); + this.gridRightButton = new DropButton(0, 87, "NAV_RIGHT", surface, midiProcessor); + this.gridUpButton = new DropButton(0, 85, "NAV_UP", surface, midiProcessor); + this.gridDownButton = new DropButton(0, 86, "NAV_DOWN", surface, midiProcessor); + + for (final DawControlInfo knobInfo : DAW_KNOBS) { + for (int i = 0; i < 8; i++) { + mappingKnobs.add( + new DropRemoteMapControl(i, 0, knobInfo.getValue(i), knobInfo.name(), surface, midiProcessor)); + } + } + for (final DawControlInfo knobInfo : DAW_PUSH_ENCODERS) { + for (int i = 0; i < 8; i++) { + mappingButtons.add( + new MapButton( + i, MidiStatus.NOTE_ON, 1, knobInfo.getValue(i), knobInfo.name(), surface, + midiProcessor)); + mappingKnobs.add(new DropRemoteMapControl( + i, 1, knobInfo.getValue(i), knobInfo.name() + " CC", surface, + midiProcessor)); + } + } + for (final DawControlInfo knobInfo : DAW_BUTTONS) { + for (int i = 0; i < 8; i++) { + mappingButtons.add( + new MapButton( + i, MidiStatus.NOTE_ON, 1, knobInfo.getValue(i), knobInfo.name(), surface, + midiProcessor)); + } + } + } + + public List getLayer1Buttons() { + return layer1Buttons; + } + + public List getLayer2Buttons() { + return layer2Buttons; + } + + public DropButton getGridLeftButton() { + return gridLeftButton; + } + + public DropButton getGridRightButton() { + return gridRightButton; + } + + public DropButton getGridUpButton() { + return gridUpButton; + } + + public DropButton getGridDownButton() { + return gridDownButton; + } + + public void fullButtonUpdate() { + layer1Buttons.forEach(DropColorButton::forceLightUpdate); + layer2Buttons.forEach(DropColorButton::forceLightUpdate); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/LauncherLayer.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/LauncherLayer.java new file mode 100644 index 00000000..4a702cce --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/LauncherLayer.java @@ -0,0 +1,313 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments; + +import java.util.Arrays; +import java.util.List; + +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.Scene; +import com.bitwig.extension.controller.api.SceneBank; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.neuzeitinstruments.controls.DropButton; +import com.bitwig.extensions.controllers.neuzeitinstruments.controls.DropColorButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Activate; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.values.PanelLayout; + +@Component +public class LauncherLayer extends Layer { + + private static final String DROP_SCENE_LAUNCH_OPTION = "default"; + private static final String DROP_SCENE_LAUNCH_QUANT = "1/4"; + + private final Layer launcherLayout; + private final Layer arrangerLayout; + private final DropMidiProcessor midiProcessor; + private final DropViewControl viewControl; + private final HwElements hwElements; + protected final DropColor[][] launcherColorIndex = new DropColor[4][4]; + protected final DropColor[][] arrangerColorIndex = new DropColor[5][3]; + protected final DropColor[] sceneColors = new DropColor[4]; + private PanelLayout panelLayout = PanelLayout.LAUNCHER; + private int sceneOffset; + + public LauncherLayer(final Layers layers, final DropViewControl viewControl, final HwElements hwElements, + final Application application, final DropMidiProcessor midiProcessor) { + super(layers, "LAUNCHER"); + this.midiProcessor = midiProcessor; + this.viewControl = viewControl; + this.hwElements = hwElements; + application.panelLayout().addValueObserver(this::handlePanelLayoutChanged); + midiProcessor.addDropRequestListener(this::handleDropRequest); + + launcherLayout = new Layer(layers, "LAUNCHER"); + arrangerLayout = new Layer(layers, "ARRANGER"); + final TrackBank launcherTrackBank = viewControl.getLauncherTrackBank(); + launcherTrackBank.sceneBank().scrollPosition().addValueObserver(value -> sceneOffset = value); + for (int i = 0; i < 4; i++) { + Arrays.fill(launcherColorIndex[i], DropColor.OFF); + } + for (int i = 0; i < 5; i++) { + Arrays.fill(arrangerColorIndex[i], DropColor.OFF); + } + final List layer1Buttons = hwElements.getLayer1Buttons(); + initLauncherClipLaunch(layer1Buttons, viewControl.getLauncherTrackBank()); + initArrangerClipLaunch(layer1Buttons, viewControl.getArrangerTrackBank()); + + initSceneLauncherClipLauncher( + hwElements.getLayer2Buttons(), viewControl.getRootTrack(), + viewControl.getLauncherTrackBank()); + initSceneLauncherArrangerLauncher( + hwElements.getLayer2Buttons(), viewControl.getRootTrack(), viewControl.getArrangerTrackBank()); + initNavigation(hwElements); + } + + private void handleDropRequest(final int value) { + if (value == 1) { + midiProcessor.setLayoutMode(panelLayout == PanelLayout.LAUNCHER ? 1 : 2); + hwElements.fullButtonUpdate(); + } + } + + private void initLauncherClipLaunch(final List buttonList, final TrackBank trackBank) { + for (int i = 0; i < 16; i++) { + final int trackIndex = i % 4; + final int sceneIndex = i / 4; + final DropColorButton button = buttonList.get(i); + final Track track = trackBank.getItemAt(trackIndex); + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + prepareSlot(slot); + slot.color() + .addValueObserver((r, g, b) -> launcherColorIndex[trackIndex][sceneIndex] = DropColor.fromRgb(r, g, b)); + + button.bindPressed(launcherLayout, () -> handleSlotPressed(slot)); + button.bindRelease(arrangerLayout, () -> handleSlotReleased(slot)); + button.bindLight(launcherLayout, () -> getState(track, slot, trackIndex, sceneIndex, launcherColorIndex)); + } + for (int i = 0; i < 4; i++) { + final DropColorButton launcherButton = buttonList.get(i + 16); + final Track track = trackBank.getItemAt(i); + launcherButton.bindLight(launcherLayout, () -> handleTrackStopColor(track)); + launcherButton.bindPressed(launcherLayout, track::stop); + } + } + + private void initArrangerClipLaunch(final List buttonList, final TrackBank trackBank) { + for (int i = 0; i < 15; i++) { + final int trackIndex = i / 3; + final int slotIndex = i % 3; + final Track track = trackBank.getItemAt(trackIndex); + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(slotIndex); + prepareSlot(slot); + slot.color() + .addValueObserver((r, g, b) -> arrangerColorIndex[trackIndex][slotIndex] = DropColor.fromRgb(r, g, b)); + final DropColorButton button = buttonList.get(trackIndex * 4 + slotIndex + 1); + button.bindPressed(arrangerLayout, () -> handleSlotPressed(slot)); + button.bindRelease(arrangerLayout, () -> handleSlotReleased(slot)); + button.bindLight(arrangerLayout, () -> getState(track, slot, trackIndex, slotIndex, arrangerColorIndex)); + } + for (int i = 0; i < 5; i++) { + final DropColorButton button = buttonList.get(i * 4); + final Track track = trackBank.getItemAt(i); + button.bindLight(arrangerLayout, () -> handleTrackStopColor(track)); + button.bindPressed(arrangerLayout, track::stop); + } + } + + private void initSceneLauncherClipLauncher(final List buttons, final Track rootTrack, + final TrackBank trackBank) { + for (int i = 0; i < trackBank.sceneBank().getSizeOfBank(); i++) { + final int sceneIndex = i; + trackBank.sceneBank().getItemAt(i).color() + .addValueObserver((r, g, b) -> sceneColors[sceneIndex] = DropColor.fromRgb(r, g, b, DropColor.GREEN)); + } + for (int i = 0; i < 4; i++) { + final int sceneIndex = i; + final Scene scene = trackBank.sceneBank().getItemAt(i); + final DropColorButton directButtonLauncher = buttons.get(i * 4 + 1); + final DropColorButton dropButtonLauncher = buttons.get(i * 4); + + directButtonLauncher.bindPressed(launcherLayout, scene::launch); + directButtonLauncher.bindRelease(launcherLayout, scene::launchRelease); + directButtonLauncher.bindLight(launcherLayout, () -> getSceneColor(scene, sceneIndex)); + dropButtonLauncher.bindLight(launcherLayout, () -> DropColor.RED); + buttons.get(i * 4 + 2).bindPressed(launcherLayout, () -> {}); + buttons.get(i * 4 + 2).bindLight(launcherLayout, () -> DropColor.OFF); + buttons.get(i * 4 + 3).bindLight(launcherLayout, () -> DropColor.OFF); + buttons.get(i * 4 + 3).bindPressed(launcherLayout, () -> {}); + } + for (int i = 0; i < 4; i++) { + final DropColorButton button = buttons.get(16 + i); + if (i == 0) { + button.bindPressed(launcherLayout, rootTrack::stop); + button.bindLight(launcherLayout, () -> getStopStateColor(rootTrack)); + } else { + button.bindLight(launcherLayout, () -> DropColor.OFF); + } + } + } + + private void initSceneLauncherArrangerLauncher(final List buttons, final Track rootTrack, + final TrackBank trackBank) { + final SceneBank sceneBank = trackBank.sceneBank(); + for (int i = 0; i < trackBank.sceneBank().getSizeOfBank(); i++) { + trackBank.sceneBank().getItemAt(i).exists().markInterested(); + } + for (int i = 0; i < 3; i++) { + final int sceneIndex = i; + final Scene scene = sceneBank.getItemAt(i); + final DropColorButton dropButtonLauncher = buttons.get(4 + i + 1); + final DropColorButton directLaunchButton = buttons.get(i + 1); + directLaunchButton.bindLight(arrangerLayout, () -> getSceneColor(scene, sceneIndex)); + directLaunchButton.bindPressed(arrangerLayout, scene::launch); + directLaunchButton.bindPressed(arrangerLayout, scene::launchRelease); + dropButtonLauncher.bindLight(arrangerLayout, () -> DropColor.RED); + dropButtonLauncher.bindPressed(arrangerLayout, () -> {}); + dropButtonLauncher.bindPressed(arrangerLayout, () -> {}); + } + for (int row = 2; row < 5; row++) { + for (int col = 1; col < 4; col++) { + final DropColorButton button = buttons.get(row * 4 + col); + button.bindLight(arrangerLayout, () -> DropColor.OFF); + } + } + + for (int i = 0; i < 5; i++) { + final DropColorButton button = buttons.get(i * 4); + if (i == 0) { + button.bindPressed(arrangerLayout, rootTrack::stop); + button.bindLight(arrangerLayout, () -> getStopStateColor(rootTrack)); + } else { + button.bindLight(arrangerLayout, () -> DropColor.OFF); + } + } + } + + private static DropColor getStopStateColor(final Track track) { + return track.isQueuedForStop().get() ? DropColor.WHITE.triggered() : DropColor.WHITE; + } + + private DropColor getSceneColor(final Scene scene, final int index) { + if (!scene.exists().get()) { + return DropColor.OFF; + } + if (viewControl.hasQueuedClips(sceneOffset + index)) { + return sceneColors[index].triggered(); + } + if (viewControl.hasPlayingClips(sceneOffset + index)) { + return sceneColors[index].playing(); + } + + return sceneColors[index]; + } + + private void initNavigation(final HwElements hwElements) { + final DropButton navLeftButton = hwElements.getGridLeftButton(); + final DropButton navRightButton = hwElements.getGridRightButton(); + final DropButton navUpButton = hwElements.getGridUpButton(); + final DropButton navDownButton = hwElements.getGridDownButton(); + + navLeftButton.bindRepeatHold(launcherLayout, () -> navigateTracks(-1)); + navRightButton.bindRepeatHold(launcherLayout, () -> navigateTracks(1)); + navUpButton.bindRepeatHold(launcherLayout, () -> navigateScenes(-1)); + navDownButton.bindRepeatHold(launcherLayout, () -> navigateScenes(1)); + + navLeftButton.bindRepeatHold(arrangerLayout, () -> navigateScenes(-1)); + navRightButton.bindRepeatHold(arrangerLayout, () -> navigateScenes(1)); + navUpButton.bindRepeatHold(arrangerLayout, () -> navigateTracks(-1)); + navDownButton.bindRepeatHold(arrangerLayout, () -> navigateTracks(1)); + } + + private void navigateTracks(final int dir) { + if (dir < 0) { + viewControl.getLauncherTrackBank().scrollBackwards(); + viewControl.getArrangerTrackBank().scrollBackwards(); + } else { + viewControl.getLauncherTrackBank().scrollForwards(); + viewControl.getArrangerTrackBank().scrollForwards(); + } + } + + private void navigateScenes(final int dir) { + if (dir < 0) { + viewControl.getLauncherTrackBank().sceneBank().scrollBackwards(); + viewControl.getArrangerTrackBank().sceneBank().scrollBackwards(); + } else { + viewControl.getLauncherTrackBank().sceneBank().scrollForwards(); + viewControl.getArrangerTrackBank().sceneBank().scrollForwards(); + } + } + + private static DropColor handleTrackStopColor(final Track track) { + if (track.isQueuedForStop().get()) { + return DropColor.WHITE.triggered(); + } + return DropColor.WHITE; + } + + protected DropColor getState(final Track track, final ClipLauncherSlot slot, final int trackIndex, + final int sceneIndex, final DropColor[][] colorTable) { + if (slot.hasContent().get()) { + final DropColor color = colorTable[trackIndex][sceneIndex]; + if (slot.isRecordingQueued().get()) { + return color.recording(); + } else if (slot.isRecording().get()) { + return color.recording(); + } else if (slot.isPlaybackQueued().get()) { + return color.triggered(); + } else if (slot.isStopQueued().get()) { + return color.triggered(); + } else if (slot.isPlaying().get() && track.isQueuedForStop().get()) { + return color.triggered(); + } else if (slot.isPlaying().get()) { + return color.playing(); + } + return color; + } + if (slot.isRecordingQueued().get()) { + return DropColor.RED.triggered(); + } + return DropColor.OFF; + } + + private void handlePanelLayoutChanged(final String value) { + if (value.equals("MIX")) { + this.panelLayout = PanelLayout.LAUNCHER; + } else { + this.panelLayout = PanelLayout.ARRANGER; + } + midiProcessor.setLayoutMode(panelLayout == PanelLayout.LAUNCHER ? 1 : 2); + launcherLayout.setIsActive(panelLayout == PanelLayout.LAUNCHER); + arrangerLayout.setIsActive(panelLayout == PanelLayout.ARRANGER); + viewControl.getArrangerTrackBank().setShouldShowClipLauncherFeedback(panelLayout == PanelLayout.ARRANGER); + viewControl.getLauncherTrackBank().setShouldShowClipLauncherFeedback(panelLayout == PanelLayout.LAUNCHER); + } + + protected void prepareSlot(final ClipLauncherSlot slot) { + slot.hasContent().markInterested(); + slot.isPlaying().markInterested(); + slot.isStopQueued().markInterested(); + slot.isRecordingQueued().markInterested(); + slot.isRecording().markInterested(); + slot.isPlaybackQueued().markInterested(); + slot.isSelected().markInterested(); + } + + private void handleSlotPressed(final ClipLauncherSlot slot) { + slot.launch(); + } + + private void handleSlotReleased(final ClipLauncherSlot slot) { + slot.launchRelease(); + } + + @Activate + public void init() { + this.setIsActive(true); + launcherLayout.setIsActive(panelLayout == PanelLayout.LAUNCHER); + arrangerLayout.setIsActive(panelLayout == PanelLayout.ARRANGER); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropButton.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropButton.java new file mode 100644 index 00000000..14cf307d --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropButton.java @@ -0,0 +1,84 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments.controls; + +import java.util.function.Consumer; + +import com.bitwig.extension.controller.api.HardwareActionBindable; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extensions.controllers.neuzeitinstruments.DropMidiProcessor; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.time.TimeRepeatEvent; +import com.bitwig.extensions.framework.time.TimedEvent; +import com.bitwig.extensions.framework.values.Midi; + +public class DropButton { + private TimedEvent currentTimer; + public static final int STD_REPEAT_DELAY = 400; + public static final int STD_REPEAT_FREQUENCY = 50; + + protected static final int NOTE_STATUS = Midi.NOTE_ON | 0xF; + protected final DropMidiProcessor midiProcessor; + protected final HardwareButton hwButton; + protected final int midiNote; + + public DropButton(final int index, final int midiNote, final String name, final HardwareSurface surface, + final DropMidiProcessor midiProcessor) { + this.midiProcessor = midiProcessor; + hwButton = surface.createHardwareButton("%s_%d".formatted(name, index + 1)); + midiProcessor.assignNoteAction(hwButton, midiNote); + this.midiNote = midiNote; + } + + public void bindIsPressed(final Layer layer, final Consumer handler) { + layer.bind(hwButton, hwButton.pressedAction(), () -> handler.accept(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> handler.accept(false)); + } + + public void bindIsPressed(final Layer layer, final SettableBooleanValue value) { + layer.bind(hwButton, hwButton.pressedAction(), () -> value.set(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> value.set(false)); + } + + public void bindPressed(final Layer layer, final Runnable action) { + layer.bind(hwButton, hwButton.pressedAction(), action); + } + + public void bindPressed(final Layer layer, final HardwareActionBindable action) { + layer.bind(hwButton, hwButton.pressedAction(), action); + } + + public void bindRelease(final Layer layer, final Runnable action) { + layer.bind(hwButton, hwButton.releasedAction(), action); + } + + /** + * Binds the given action to a button. Upon pressing the button the action is immediately executed. However while + * holding the button, the action repeats after an initial delay. The standard delay time of 400ms and repeat + * frequency of 50ms are used. + * + * @param layer the layer this is bound to + * @param action action to be invoked and after a delay repeat + */ + public void bindRepeatHold(final Layer layer, final Runnable action) { + layer.bind( + hwButton, hwButton.pressedAction(), + () -> initiateRepeat(action, STD_REPEAT_DELAY, STD_REPEAT_FREQUENCY)); + layer.bind(hwButton, hwButton.releasedAction(), this::cancelEvent); + } + + private void initiateRepeat(final Runnable action, final int repeatDelay, final int repeatFrequency) { + action.run(); + currentTimer = new TimeRepeatEvent(action, repeatDelay, repeatFrequency); + midiProcessor.queueEvent(currentTimer); + } + + private void cancelEvent() { + if (currentTimer != null) { + currentTimer.cancel(); + currentTimer = null; + } + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropColorButton.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropColorButton.java new file mode 100644 index 00000000..c01f98ef --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropColorButton.java @@ -0,0 +1,46 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments.controls; + +import java.util.function.Supplier; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.neuzeitinstruments.DropColor; +import com.bitwig.extensions.controllers.neuzeitinstruments.DropMidiProcessor; +import com.bitwig.extensions.framework.Layer; + +public class DropColorButton extends DropButton { + + private final MultiStateHardwareLight light; + private int lastColorIndex = -1; + + public DropColorButton(final int index, final int midiNote, final String name, final HardwareSurface surface, + final DropMidiProcessor midiProcessor) { + super(index, midiNote, name, surface, midiProcessor); + light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + midiNote); + light.state().setValue(RgbLightState.OFF); + hwButton.setBackgroundLight(light); + hwButton.isPressed().markInterested(); + light.state().onUpdateHardware(this::handleState); + } + + private void handleState(final InternalHardwareLightState state) { + if (state instanceof final DropColor color) { + midiProcessor.sendMidi(NOTE_STATUS, midiNote, color.getColorIndex()); + this.lastColorIndex = color.getColorIndex(); + } else { + midiProcessor.sendMidi(NOTE_STATUS, midiNote, 0); + } + } + + public void forceLightUpdate() { + if (lastColorIndex != -1) { + midiProcessor.sendMidi(NOTE_STATUS, midiNote, lastColorIndex); + } + } + + public void bindLight(final Layer layer, final Supplier supplier) { + layer.bindLightState(supplier, light); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropRemoteMapControl.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropRemoteMapControl.java new file mode 100644 index 00000000..b0286765 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/DropRemoteMapControl.java @@ -0,0 +1,37 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments.controls; + +import com.bitwig.extension.controller.api.AbsoluteHardwareKnob; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extensions.controllers.neuzeitinstruments.DropMidiProcessor; +import com.bitwig.extensions.framework.values.Midi; + +public class DropRemoteMapControl { + + private final DropMidiProcessor midiProcessor; + private final int ccNr; + private long lastUpdate; + private final int channel; + + public DropRemoteMapControl(final int index, final int channel, final int ccNr, final String name, + final HardwareSurface surface, final DropMidiProcessor midiProcessor) { + this.midiProcessor = midiProcessor; + this.ccNr = ccNr; + this.channel = channel; + final AbsoluteHardwareKnob control = surface.createAbsoluteHardwareKnob("%s-%d".formatted(name, index + 1)); + midiProcessor.assignCcMatcher(control, channel, ccNr); + control.value().addValueObserver(this::handleKnobAction); + control.targetValue().addValueObserver(this::handleTargetValueUpdate); + } + + private void handleKnobAction(final double v) { + this.lastUpdate = System.currentTimeMillis(); + } + + private void handleTargetValueUpdate(final double value) { + final int targetValue = (int) Math.round(value * 127); + final long diff = System.currentTimeMillis() - lastUpdate; + if (diff > 1000) { + midiProcessor.sendMidi(Midi.CC | channel, ccNr, targetValue); + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/MapButton.java b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/MapButton.java new file mode 100644 index 00000000..728e8d9d --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/neuzeitinstruments/controls/MapButton.java @@ -0,0 +1,40 @@ +package com.bitwig.extensions.controllers.neuzeitinstruments.controls; + +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.OnOffHardwareLight; +import com.bitwig.extensions.controllers.neuzeitinstruments.DropMidiProcessor; +import com.bitwig.extensions.framework.values.MidiStatus; + +public class MapButton { + private final int ccNr; + private final HardwareButton button; + private final int midiStatusValue; + + public MapButton(final int index, final MidiStatus midiStatus, final int channel, final int noteNr, + final String name, final HardwareSurface surface, final DropMidiProcessor midiProcessor) { + this.ccNr = noteNr; + this.midiStatusValue = midiStatus.getStatus(channel); + button = surface.createHardwareButton("%s-%d".formatted(name, index + 1)); + midiProcessor.assignCcAction(button, channel, noteNr); + + final MidiIn midiIn = midiProcessor.getMidiIn(); + if (midiStatus == MidiStatus.NOTE_ON) { + button.pressedAction().setActionMatcher(midiIn.createNoteOnActionMatcher(channel, noteNr)); + button.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, noteNr)); + } else { + button.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, noteNr, 0x7F)); + button.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, noteNr, 0x0)); + } + + final OnOffHardwareLight light = surface.createOnOffHardwareLight("%s-light-%d".formatted(name, index + 1)); + button.setBackgroundLight(light); + button.isPressed().markInterested(); + + light.onUpdateHardware(() -> { + midiProcessor.sendMidi(midiStatusValue, noteNr, light.isOn().currentValue() ? 0x7F : 0); + }); + } + +} diff --git a/src/main/java/com/bitwig/extensions/framework/values/MidiStatus.java b/src/main/java/com/bitwig/extensions/framework/values/MidiStatus.java new file mode 100644 index 00000000..482545af --- /dev/null +++ b/src/main/java/com/bitwig/extensions/framework/values/MidiStatus.java @@ -0,0 +1,22 @@ +package com.bitwig.extensions.framework.values; + +public enum MidiStatus { + + NOTE_OFF(0x80), + NOTE_ON(0x90), + CC(0xB0); + + private final int value; + + MidiStatus(final int value) { + this.value = value; + } + + public int getStatus(final int channel) { + return this.value | channel; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/com/bitwig/extensions/framework/values/PanelLayout.java b/src/main/java/com/bitwig/extensions/framework/values/PanelLayout.java new file mode 100644 index 00000000..f7fbd20a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/framework/values/PanelLayout.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.framework.values; + +public enum PanelLayout { + LAUNCHER, + ARRANGER +} diff --git a/src/main/java/com/bitwig/extensions/framework/view/OverviewGrid.java b/src/main/java/com/bitwig/extensions/framework/view/OverviewGrid.java new file mode 100644 index 00000000..87cb488e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/framework/view/OverviewGrid.java @@ -0,0 +1,160 @@ +package com.bitwig.extensions.framework.view; + +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.Scene; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; + +public class OverviewGrid { + + private int sceneOffset; + private int trackOffset; + private int numberOfScenes; + private int numberOfTracks; + + private int trackPosition; + private int scenePosition; + + private final int[] sceneQueuedClips; + private final int[] playingClips; + private final int[] clipCount; + private final int size; + + private final TrackBank maxTrackBank; + + public OverviewGrid(final TrackBank maxTrackBank) { + this.size = maxTrackBank.getSizeOfBank(); + this.maxTrackBank = maxTrackBank; + sceneQueuedClips = new int[size]; + playingClips = new int[size]; + clipCount = new int[size]; + prepare(maxTrackBank); + } + + public void setUpFocusScene(final TrackBank mainBank) { + prepare(mainBank); + mainBank.sceneBank().itemCount().addValueObserver(this::setNumberOfScenes); + mainBank.channelCount().addValueObserver(this::setNumberOfTracks); + mainBank.scrollPosition().addValueObserver(pos -> { + setTrackPosition(pos); + if (maxTrackBank.scrollPosition().get() != getTrackOffset()) { + maxTrackBank.scrollPosition().set(getTrackOffset()); + } + }); + mainBank.sceneBank().scrollPosition().addValueObserver(pos -> { + setScenePosition(pos); + if (maxTrackBank.sceneBank().scrollPosition().get() != getSceneOffset()) { + maxTrackBank.sceneBank().scrollPosition().set(getSceneOffset()); + } + }); + for (int i = 0; i < size; i++) { + final int trackIndex = i; + final Track track = maxTrackBank.getItemAt(trackIndex); + + final Scene scene = maxTrackBank.sceneBank().getScene(i); + scene.clipCount().addValueObserver(count -> this.clipCount[trackIndex] = count); + for (int j = 0; j < size; j++) { + final int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + slot.isPlaybackQueued().addValueObserver(isQueued -> markSceneQueued(sceneIndex, isQueued)); + slot.isPlaying().addValueObserver(isQueued -> markScenePlaying(sceneIndex, isQueued)); + } + } + } + + public int getNumberOfScenes() { + return numberOfScenes; + } + + public void setNumberOfScenes(final int numberOfScenes) { + this.numberOfScenes = numberOfScenes; + } + + public int getNumberOfTracks() { + return numberOfTracks; + } + + public void setNumberOfTracks(final int numberOfTracks) { + this.numberOfTracks = numberOfTracks; + } + + public int getTrackPosition() { + return trackPosition - trackOffset; + } + + public int getTrackOffset() { + return trackOffset; + } + + public void setTrackPosition(final int trackPosition) { + this.trackPosition = trackPosition; + this.trackOffset = (trackPosition / size) * size; + } + + public int getScenePosition() { + return scenePosition - sceneOffset; + } + + public void setScenePosition(final int scenePosition) { + this.scenePosition = scenePosition; + this.sceneOffset = (scenePosition / size) * size; + } + + public int getSceneOffset() { + return sceneOffset; + } + + private void markSceneQueued(final int sceneIndex, final boolean isQueued) { + if (isQueued) { + sceneQueuedClips[sceneIndex]++; + } else if (sceneQueuedClips[sceneIndex] > 0) { + sceneQueuedClips[sceneIndex]--; + } + } + + private void markScenePlaying(final int sceneIndex, final boolean isPlaying) { + if (isPlaying) { + playingClips[sceneIndex]++; + } else if (playingClips[sceneIndex] > 0) { + playingClips[sceneIndex]--; + } + } + + public boolean hasClips(final int sceneIndex) { + return this.clipCount[sceneIndex] > 0; + } + + + public boolean hasQueuedScenes(final int sceneIndex) { + final int index = sceneIndex - sceneOffset; + if (index >= size) { + return false; + } + return this.sceneQueuedClips[index] > 0; + } + + public boolean hasPlayingClips(final int sceneIndex) { + final int index = sceneIndex - sceneOffset; + if (index >= size) { + return false; + } + return this.playingClips[index] > 0; + } + + + public boolean inGrid(final int trackIndex, final int sceneIndex) { + final int posX = trackIndex * 8; + final int posY = sceneIndex * 8; + return posX < (numberOfTracks - trackOffset) && posY < (numberOfScenes - sceneOffset); + } + + + private static void prepare(final TrackBank bank) { + bank.scrollPosition().markInterested(); + bank.sceneBank().scrollPosition().markInterested(); + for (int i = 0; i < bank.sceneBank().getSizeOfBank(); i++) { + final Scene scene = bank.sceneBank().getScene(i); + scene.exists().markInterested(); + } + } +} diff --git a/src/main/resources/Documentation/Controllers/Neuzeit Instruments/Neuzeit Instruments Drop.pdf b/src/main/resources/Documentation/Controllers/Neuzeit Instruments/Neuzeit Instruments Drop.pdf new file mode 100644 index 00000000..c5c872a4 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/Neuzeit Instruments/Neuzeit Instruments Drop.pdf differ diff --git a/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition index 9a7262f4..7f88096e 100644 --- a/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition +++ b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition @@ -68,6 +68,8 @@ com.bitwig.extensions.controllers.nativeinstruments.komplete.definition.Komplete com.bitwig.extensions.controllers.nativeinstruments.komplete.definition.KompleteKontrolMSeriesExtensionDefinition com.bitwig.extensions.controllers.nativeinstruments.komplete.definition.KontrolSMk3ExtensionDefinition +com.bitwig.extensions.controllers.neuzeitinstruments.DropExtensionDefinition + com.bitwig.extensions.controllers.mackie.definition.MackieMcuProExtensionDefinition com.bitwig.extensions.controllers.mackie.definition.MackieMcuProXt1ExtensionDefinition com.bitwig.extensions.controllers.mackie.definition.MackieMcuProXt2ExtensionDefinition