diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ApcButton.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ApcButton.java index 69f89083..ee921430 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ApcButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ApcButton.java @@ -1,6 +1,15 @@ package com.bitwig.extensions.controllers.akai.apc.common.control; -import com.bitwig.extension.controller.api.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +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.InternalHardwareLightState; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; import com.bitwig.extensions.framework.Layer; @@ -8,23 +17,19 @@ import com.bitwig.extensions.framework.time.TimedDelayEvent; import com.bitwig.extensions.framework.time.TimedEvent; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - public abstract class ApcButton { public static final int STD_REPEAT_DELAY = 400; public static final int STD_REPEAT_FREQUENCY = 50; - + protected MultiStateHardwareLight light; protected HardwareButton hwButton; protected MidiProcessor midiProcessor; private TimedEvent currentTimer; private long recordedDownTime; protected final int midiId; - + protected ApcButton(final int channel, final int midiId, final String name, final HardwareSurface surface, - final MidiProcessor midiProcessor) { + final MidiProcessor midiProcessor) { this.midiProcessor = midiProcessor; final MidiIn midiIn = midiProcessor.getMidiIn(); this.midiId = midiId; @@ -36,69 +41,71 @@ protected ApcButton(final int channel, final int midiId, final String name, fina hwButton.setBackgroundLight(light); hwButton.isPressed().markInterested(); } - - + public void refresh() { light.state().setValue(null); } - + 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 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); } - + public void bindLight(final Layer layer, final Supplier supplier) { layer.bindLightState(supplier, light); } - + public void bindLightPressed(final Layer layer, final Function supplier) { layer.bindLightState(() -> supplier.apply(hwButton.isPressed().get()), light); } - + public void bindLight(final Layer layer, final Function pressedCombine) { layer.bindLightState(() -> pressedCombine.apply(hwButton.isPressed().get()), light); } - + public void bindLightPressed(final Layer layer, final InternalHardwareLightState state, - final InternalHardwareLightState holdState) { + final InternalHardwareLightState holdState) { layer.bindLightState(() -> hwButton.isPressed().get() ? holdState : state, light); } - + /** - * Models following behavior. Pressing and Releasing the button within the given delay time executes the click event. + * Models following behavior. Pressing and Releasing the button within the given delay time executes the click + * event. * Long Pressing the button invokes the holdAction with true and then the same action with false once released. * * @param layer the layer * @param clickAction the action invoked if the button is pressed and release in less than the given delay time - * @param holdAction action called with true when the delay time expires and with false if released under this condition + * @param holdAction action called with true when the delay time expires and with false if released under this + * condition * @param delayTime the delay time */ public void bindDelayedHold(final Layer layer, final Runnable clickAction, final Consumer holdAction, - final long delayTime) { + final long delayTime) { layer.bind(hwButton, hwButton.pressedAction(), () -> initiateHold(holdAction, delayTime)); layer.bind(hwButton, hwButton.releasedAction(), () -> handleDelayedRelease(clickAction, holdAction)); } - + private void initiateHold(final Consumer holdAction, final long delayTime) { recordedDownTime = System.currentTimeMillis(); - currentTimer = new TimedDelayEvent(() -> { - holdAction.accept(true); - }, delayTime); + currentTimer = new TimedDelayEvent( + () -> { + holdAction.accept(true); + }, delayTime); midiProcessor.queueEvent(currentTimer); } - + private void handleDelayedRelease(final Runnable clickAction, final Consumer holdAction) { if (currentTimer != null && !currentTimer.isCompleted()) { currentTimer.cancel(); @@ -108,7 +115,7 @@ private void handleDelayedRelease(final Runnable clickAction, final Consumer initiateRepeat(action, STD_REPEAT_DELAY, STD_REPEAT_FREQUENCY)); + layer.bind( + hwButton, hwButton.pressedAction(), + () -> initiateRepeat(action, STD_REPEAT_DELAY, STD_REPEAT_FREQUENCY)); layer.bind(hwButton, hwButton.releasedAction(), this::cancelEvent); } - + public 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/akai/apc64/StringUtil.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/StringUtil.java index 78d53e69..2f7465ef 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/StringUtil.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/StringUtil.java @@ -1,11 +1,15 @@ package com.bitwig.extensions.controllers.akai.apc64; public class StringUtil { - private static final char[] SPECIALS = {'ä', 'ü', 'ö', 'Ä', 'Ü', 'Ö', 'ß', 'é', 'è', 'ê', 'â', 'á', 'à', // - 'û', 'ú', 'ù', 'ô', 'ó', 'ò'}; - private static final String[] REPLACE = {"a", "u", "o", "A", "U", "O", "ss", "e", "e", "e", "a", "a", "a", // - "u", "u", "u", "o", "o", "o"}; - + private static final char[] SPECIALS = { + 'ä', 'ü', 'ö', 'Ä', 'Ü', 'Ö', 'ß', 'é', 'è', 'ê', 'â', 'á', 'à', // + 'û', 'ú', 'ù', 'ô', 'ó', 'ò' + }; + private static final String[] REPLACE = { + "a", "u", "o", "A", "U", "O", "s", "e", "e", "e", "a", "a", "a", // + "u", "u", "u", "o", "o", "o" + }; + public static String nextValue(final String currentValue, final String[] list, final int inc, final boolean wrap) { int index = -1; for (int i = 0; i < list.length; i++) { @@ -25,14 +29,14 @@ public static String nextValue(final String currentValue, final String[] list, f } return list[0]; } - + public static String toAsciiDisplay(final String name, final int maxLen) { final StringBuilder b = new StringBuilder(); for (int i = 0; i < name.length() && b.length() < maxLen; i++) { final char c = name.charAt(i); -// if (c == 32) { -// continue; -// } + // if (c == 32) { + // continue; + // } if (c < 128) { b.append(c); } else { @@ -44,7 +48,7 @@ public static String toAsciiDisplay(final String name, final int maxLen) { } return b.toString(); } - + private static int getReplace(final char c) { for (int i = 0; i < SPECIALS.length; i++) { if (c == SPECIALS[i]) { @@ -53,6 +57,6 @@ private static int getReplace(final char c) { } return -1; } - - + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/GlobalStates.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/GlobalStates.java new file mode 100644 index 00000000..c4dd5e8f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/GlobalStates.java @@ -0,0 +1,27 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4; + +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +public class GlobalStates { + + private final BooleanValueObject shiftHeld = new BooleanValueObject(); + private final MpkMk4ControllerExtension.Variant variant; + private final BasicStringValue focusDeviceName = new BasicStringValue(); + + public GlobalStates(final MpkMk4ControllerExtension.Variant variant) { + this.variant = variant; + } + + public MpkMk4ControllerExtension.Variant getVariant() { + return variant; + } + + public BooleanValueObject getShiftHeld() { + return shiftHeld; + } + + public BasicStringValue getFocusDeviceName() { + return focusDeviceName; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkFocusClip.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkFocusClip.java new file mode 100644 index 00000000..51e71b62 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkFocusClip.java @@ -0,0 +1,103 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4; + +import java.util.Optional; + +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.Transport; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class MpkFocusClip { + private final Clip mainCursoClip; + private final int slotRange; + private final CursorTrack cursorTrack; + private final Transport transport; + private final ClipLauncherSlotBank slotBank; + private int selectedSlotIndex; + + public MpkFocusClip(final ControllerHost host, final Transport transport, final MpkViewControl viewControl) { + this.cursorTrack = viewControl.getCursorTrack(); + this.transport = transport; + mainCursoClip = host.createLauncherCursorClip(16, 1); + slotBank = cursorTrack.clipLauncherSlotBank(); + slotRange = slotBank.getSizeOfBank(); + for (int i = 0; i < slotBank.getSizeOfBank(); i++) { + final int index = i; + final ClipLauncherSlot slot = slotBank.getItemAt(i); + slot.isRecording().markInterested(); + slot.isPlaying().markInterested(); + slot.hasContent().markInterested(); + slot.exists().markInterested(); + slot.isSelected().addValueObserver(selected -> this.handleSelection(selected, index)); + } + } + + private void handleSelection(final boolean selected, final int index) { + if (selected) { + this.selectedSlotIndex = index; + } + } + + public void setSelectedSlotIndex(final int pos) { + this.selectedSlotIndex = pos; + if (selectedSlotIndex < slotBank.getSizeOfBank()) { + slotBank.getItemAt(selectedSlotIndex).select(); + } + } + + public void invokeRecord() { + if (selectedSlotIndex != -1) { + final ClipLauncherSlot slot = slotBank.getItemAt(selectedSlotIndex); + if (!transport.isPlaying().get()) { + transport.play(); + } + if (slot.isRecording().get()) { + slot.launch(); + transport.isClipLauncherOverdubEnabled().set(false); + } else if (slot.isPlaying().get()) { + transport.isClipLauncherOverdubEnabled().toggle(); + } else { + slot.launch(); + transport.isClipLauncherOverdubEnabled().set(true); + } + } else { + findNextEmptySlot(true).ifPresent(slot -> { + slot.launch(); + transport.isClipLauncherOverdubEnabled().set(true); + }); + } + } + + private Optional findNextEmptySlot(final boolean select) { + ClipLauncherSlot lastEmpty = null; + int lastEmptyIndex = -1; + for (int i = slotRange - 1; i >= 0; i--) { + final ClipLauncherSlot slot = slotBank.getItemAt(i); + if (slot.hasContent().get()) { + if (lastEmpty != null) { + break; + } + } else { + lastEmpty = slot; + lastEmptyIndex = i; + } + } + if (lastEmpty != null) { + if (select) { + slotBank.select(lastEmptyIndex); + } + return Optional.of(lastEmpty); + } + return Optional.empty(); + } + + public void quantize(final double amount) { + mainCursoClip.quantize(amount); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkHwElements.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkHwElements.java new file mode 100644 index 00000000..d1bdd8f6 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkHwElements.java @@ -0,0 +1,97 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extensions.controllers.akai.apc.common.control.ClickEncoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.Encoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkCcAssignment; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkMultiStateButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.LineDisplay; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkDisplayFont; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class MpkHwElements { + + private final List gridButtons = new ArrayList<>(); + private final List encoders = new ArrayList<>(); + private final Map onOffButtons = new HashMap<>(); + private final ClickEncoder mainEncoder; + private final MpkButton mainEncoderPressButton; + private final MpkButton shiftButton; + private final LineDisplay mainLineDisplay; + private final LineDisplay menuLineDisplay; + + public MpkHwElements(final ControllerHost host, final HardwareSurface surface, final MpkMidiProcessor midiProcessor, + final GlobalStates globalStates) { + for (int i = 0; i < 16; i++) { + final int rowIndex = i / 4; + final int columnIndex = i % 4; + final String name = "GRID %d %d".formatted(rowIndex + 1, columnIndex + 1); + gridButtons.add(new MpkMultiStateButton(9, 0x24 + i, false, name, surface, midiProcessor)); + } + + mainLineDisplay = new LineDisplay(midiProcessor, MpkDisplayFont.PT24, 3); + menuLineDisplay = new LineDisplay(midiProcessor, MpkDisplayFont.PT24, 3); + + mainEncoder = new ClickEncoder(0xE, host, surface, midiProcessor.getDawMidiIn()); + mainEncoderPressButton = new MpkButton(0, 0xD, true, "ENCODER_PRESS", surface, midiProcessor); + shiftButton = new MpkButton(0, 0x11, true, "SHIFT", surface, midiProcessor); + for (final MpkCcAssignment assignment : MpkCcAssignment.values()) { + final MpkMultiStateButton button = + new MpkMultiStateButton(0, assignment.getCcNr(), true, assignment.toString(), surface, midiProcessor); + onOffButtons.put(assignment, button); + } + for (int i = 0; i < 8; i++) { + final Encoder encoder = new Encoder(i, 0x18 + i, surface, midiProcessor.getDawMidiIn()); + encoders.add(encoder); + } + mainLineDisplay.setActive(true); + } + + public LineDisplay getMainLineDisplay() { + return mainLineDisplay; + } + + public LineDisplay getMenuLineDisplay() { + return menuLineDisplay; + } + + public List getGridButtons() { + return gridButtons; + } + + public ClickEncoder getMainEncoder() { + return mainEncoder; + } + + public MpkButton getShiftButton() { + return shiftButton; + } + + public MpkButton getMainEncoderPressButton() { + return mainEncoderPressButton; + } + + public MpkMultiStateButton getButton(final MpkCcAssignment assignment) { + return onOffButtons.get(assignment); + } + + public List getEncoders() { + return encoders; + } + + public void applyShiftToEncoders(final boolean shiftHeld) { + for (final Encoder encoder : encoders) { + //encoder.getEncoder().setStepSize(shiftHeld ? 0.1 : 0.01); + encoder.getEncoder().setSensitivity(shiftHeld ? 0.5 : 1.0); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMidiProcessor.java new file mode 100644 index 00000000..d0636a02 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMidiProcessor.java @@ -0,0 +1,274 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.IntConsumer; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.LineDisplay; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkDisplayFont; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.ScreenRowState; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.StringUtil; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.time.TimedEvent; +import com.bitwig.extensions.framework.values.Midi; + +@Component +public class MpkMidiProcessor { + + private static final String DEVICE_INQUIRY = "F0 7E 7F 06 01 F7"; + private static final String DEVICE_RESPONSE_HEADER = "f07e7f0602475d0019"; + + private static final String IN_SYSEX_HEADER = "f0477f5d"; + + private static final String IN_MODE_SYSEX = IN_SYSEX_HEADER + "2a00"; + private static final String IN_SCREEN_OWNER_SHIP = IN_SYSEX_HEADER + "19000100f7"; + private static final String IN_DAW_MODE_SYSEX = IN_SYSEX_HEADER + "190000f7"; + private static final String IN_SCREEN_STATUS = IN_SYSEX_HEADER + "190011"; + // SYSEX = f0477f5d2d000100f7 + // SYSEX = f0477f5d2b000101f7 + + + private static final String AKAI_HEADER = "F0 47 7F 5D "; + private static final String SET_PRESET_DAW = AKAI_HEADER + "2D 00 00 F7"; + private static final String SET_SCREEN_OWNER = AKAI_HEADER + "1C 00 01 02 F7"; + private static final String SET_SCREEN_FW = AKAI_HEADER + "1C 00 01 00 F7"; + private static final String SET_MODE_CLIP = AKAI_HEADER + "2A 00 01 01 F7"; + private static final String SET_DISPLAY_STRING = AKAI_HEADER + "10 "; + private static final String SET_DISPLAY_COLOR = AKAI_HEADER + "11 00 02 %02X %02X F7"; + private static final String SET_DISPLAY_ROW = AKAI_HEADER + "14 "; + private static final String SET_PAD_NOTES_ENABLED = AKAI_HEADER + "2E 00 01 %s F7"; + private static final String DISABLE_PADS = AKAI_HEADER + "2B 00 01 01 F7"; + private static final byte[] ROW_DISPLAY_COLOR = prepareFixedData(0x14, 9); + private static final byte[] CLEAR_LINE_TEXT = prepareFixedData(0x15, 4); + + private final ControllerHost host; + private final MidiIn dawMidiIn; + private final MidiOut dawMidiOut; + private final MidiIn playMidiIn; + private final GlobalStates globalStates; + private LineDisplay mainDisplay; + private final NoteInput noteInput; + protected final Queue timedEvents = new ConcurrentLinkedQueue<>(); + private final List updateListeners = new ArrayList<>(); + private final List modeChangeListeners = new ArrayList<>(); + private long lastScreenRequest = System.currentTimeMillis(); + private final List noteListeners = new ArrayList<>(); + + @FunctionalInterface + public interface NoteListener { + void playNote(int note, int vel); + } + + public MpkMidiProcessor(final ControllerHost host, final GlobalStates globalStates) { + this.host = host; + this.globalStates = globalStates; + this.dawMidiIn = host.getMidiInPort(0); + noteInput = dawMidiIn.createNoteInput("MIDI", "89????", "99????", "A9????"); + noteInput.setShouldConsumeEvents(false); + this.dawMidiOut = host.getMidiOutPort(0); + this.playMidiIn = host.getMidiInPort(1); + + playMidiIn.createNoteInput("IN", "??????"); + this.dawMidiIn.setSysexCallback(this::handleSysEx); + this.dawMidiIn.setMidiCallback(this::handleMidiIn); + } + + private static byte[] prepareFixedData(final int commandId, final int fixedPayLoad) { + final byte[] data = new byte[8 + fixedPayLoad]; + data[0] = (byte) 0xF0; + data[1] = (byte) 0x47; + data[2] = (byte) 0x7F; + data[3] = (byte) 0x5D; + data[4] = (byte) commandId; + data[5] = (byte) 0x00; + data[6] = (byte) fixedPayLoad; + data[data.length - 1] = (byte) 0xF7; + return data; + } + + public void init() { + dawMidiOut.sendSysex(DEVICE_INQUIRY); + } + + public MidiIn getDawMidiIn() { + return dawMidiIn; + } + + public void queueEvent(final TimedEvent event) { + timedEvents.add(event); + } + + public void addUpdateListeners(final Runnable updateListener) { + this.updateListeners.add(updateListener); + } + + public void addNoteListener(final NoteListener listener) { + noteListeners.add(listener); + } + + private void handlePing() { + final long now = System.currentTimeMillis(); + if (!timedEvents.isEmpty()) { + for (final TimedEvent event : timedEvents) { + event.process(); + if (event.isCompleted()) { + timedEvents.remove(event); + } + } + } + final long diff = System.currentTimeMillis() - lastScreenRequest; + if (diff > 5800) { + dawMidiOut.sendSysex(SET_SCREEN_OWNER); + lastScreenRequest = now; + } + host.scheduleTask(this::handlePing, 50); + } + + public NoteInput getNoteInput() { + return noteInput; + } + + private void handleSysEx(final String data) { + if (data.startsWith(DEVICE_RESPONSE_HEADER)) { + MpkMk4ControllerExtension.println(" Connected !"); + startConnection(); + } else if (data.startsWith(IN_SCREEN_OWNER_SHIP)) { + //MpkMk4ControllerExtension.println(" IN OWNERSHIP "); + //dawMidiOut.sendSysex(SET_SCREEN_OWNER); + } else if (data.startsWith(IN_SCREEN_STATUS)) { + updateListeners.forEach(l -> l.run()); + } else if (data.startsWith(IN_MODE_SYSEX)) { + final int padMode = getValue(data, 7); + modeChangeListeners.forEach(l -> l.accept(padMode)); + } else if (data.startsWith(IN_DAW_MODE_SYSEX)) { + // final int modeValue = getValue(data, 6); + // if (modeValue == 0) { + // updateListeners.forEach(l -> l.run()); + // } + } else if (data.startsWith(IN_SYSEX_HEADER)) { + final int command = getValue(data, 4); + final int payload = getPayload(data); + final int value = getValue(data, 7); + MpkMk4ControllerExtension.println(" IN Command = %02X PL=%d v=%d", command, payload, value); + } else { + MpkMk4ControllerExtension.println(" SYSEX = %s", data); + } + } + + public void setPadNotesEnabled(final boolean enabled) { + dawMidiOut.sendSysex(SET_PAD_NOTES_ENABLED.formatted(enabled ? "00" : "01")); + } + + public void addModeChangeListener(final IntConsumer listener) { + this.modeChangeListeners.add(listener); + } + + public void registerMainDisplay(final LineDisplay mainLineDisplay) { + this.mainDisplay = mainLineDisplay; + } + + private void startConnection() { + dawMidiOut.sendSysex(SET_PRESET_DAW); + dawMidiOut.sendSysex(SET_MODE_CLIP); + dawMidiOut.sendSysex(SET_SCREEN_OWNER); + dawMidiOut.sendSysex(AKAI_HEADER + "2B 00 00 F7"); + dawMidiOut.sendSysex(AKAI_HEADER + "3A 00 00 F7"); + setPadNotesEnabled(false); + mainDisplay.updateCurrent(); + host.scheduleTask(this::handlePing, 50); + } + + private void handleMidiIn(final int status, final int data1, final int data2) { + //MpkMk4ControllerExtension.println(" MIDI in %02X %02X %02X", status, data1, data2); + if (status == (Midi.NOTE_ON | 9) || status == (Midi.NOTE_OFF | 9)) { + noteListeners.forEach(l -> l.playNote(data1, data2)); + } + } + + public void sendMidi(final int status, final int val1, final int val2) { + dawMidiOut.sendMidi(status, val1, val2); + } + + public void configureLine(final MpkDisplayFont font, final int lineIndex, final int justification, + final Color foreGround, final Color background) { + final String sb = SET_DISPLAY_ROW + "00 09 " // + + "%02X ".formatted(font.getValue()) // + + "%02X ".formatted(lineIndex) // + + "%02X ".formatted(justification) // + + "%02X ".formatted(foreGround.getRed255() >> 3) // + + "%02X ".formatted(foreGround.getGreen255() >> 2) // + + "%02X ".formatted(foreGround.getBlue255() >> 3) // + + "%02X ".formatted(background.getRed255() >> 3) // + + "%02X ".formatted(background.getGreen255() >> 2) // + + "%02X ".formatted(background.getBlue255() >> 3) // + + "F7"; + dawMidiOut.sendSysex(sb); + } + + public void setText(final int line, final MpkDisplayFont font, final String text) { + final StringBuilder sb = new StringBuilder(SET_DISPLAY_STRING); + final String asciiText = StringUtil.toAsciiDisplay(text, 31); + sb.append("%02X ".formatted(0)); + final int length = asciiText.length(); + sb.append("%02X ".formatted(length + 3)); + sb.append("%02X ".formatted(font.getValue())); + sb.append("%02X ".formatted(line)); + for (int i = 0; i < length; i++) { + sb.append("%02X ".formatted((int) asciiText.charAt(i))); + } + sb.append("00 "); + sb.append("F7"); + dawMidiOut.sendSysex(sb.toString()); + } + + public void setRowDisplayColor(final ScreenRowState rowState) { + // ROW_DISPLAY_COLOR + // Screen State + } + + public void clearLineText(final int lineIndex, final int red, final int green, final int blue) { + + } + + public void sendSysEx(final byte[] data) { + //MpkMk4ControllerExtension.println(" :: %s", StringUtil.sysExString(data)); + dawMidiOut.sendSysex(data); + } + + public void setDisplayColor(final int line, final int color) { + dawMidiOut.sendSysex(SET_DISPLAY_COLOR.formatted(line, color)); + } + + public void exit() { + dawMidiOut.sendSysex(SET_SCREEN_FW); + setPadNotesEnabled(true); + } + + private static int getValue(final String data, final int byteOffset) { + //MpkMk4ControllerExtension.println(" ==> = %s", data); + final int stringOffset = byteOffset * 2; + if (stringOffset < data.length()) { + final String stringValue = data.substring(stringOffset, stringOffset + 2); + if ("F7".equals(stringValue)) { + return -1; + } + return Integer.parseInt(stringValue, 16); + } + return -1; + } + + private static int getPayload(final String data) { + //MpkMk4ControllerExtension.println(" ==> = %s", data); + final int high = getValue(data, 5); + final int low = getValue(data, 6); + return high << 7 | low; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMiniMk4ControllerExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMiniMk4ControllerExtensionDefinition.java new file mode 100644 index 00000000..1fc9b5dd --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMiniMk4ControllerExtensionDefinition.java @@ -0,0 +1,89 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.ControllerExtension; +import com.bitwig.extension.controller.ControllerExtensionDefinition; +import com.bitwig.extension.controller.api.ControllerHost; + +public class MpkMiniMk4ControllerExtensionDefinition extends ControllerExtensionDefinition { + + private final static UUID ID = UUID.fromString("3ad31d42-81d0-4603-8719-d0462633d943"); + + @Override + public String getHardwareVendor() { + return "Akai"; + } + + @Override + public String getHardwareModel() { + return "MPK Mini IV"; + } + + @Override + public int getNumMidiInPorts() { + return 2; + } + + @Override + public int getNumMidiOutPorts() { + return 2; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { + list.add( + new String[] {"MIDIIN2 (MPK mini IV)", "MPK mini IV"}, + new String[] {"MIDIOUT2 (MPK mini IV)", "MPK mini IV"}); + } else if (platformType == PlatformType.MAC) { + list.add( + new String[] {"MPK mini IV DAW Port", "MPK mini IV MIDI Port"}, + new String[] {"MPK mini IV DAW Port", "MPK mini IV MIDI Port"}); + } else if (platformType == PlatformType.LINUX) { + list.add( + new String[] {"MPK mini IV MPK mini IV DAW Por", "MPK mini IV MPK mini IV MIDI Po"}, + new String[] {"MPK mini IV DAW Port", "MPK mini IV MIDI Port"}); + } + } + + @Override + public ControllerExtension createInstance(final ControllerHost host) { + return new MpkMk4ControllerExtension(this, host, MpkMk4ControllerExtension.Variant.MINI); + } + + @Override + public String getHelpFilePath() { + return "Controllers/Akai/MPK Mini Mk4.pdf"; + } + + @Override + public String getName() { + return "MPK Mini IV"; + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "1.0"; + } + + @Override + public UUID getId() { + return ID; + } + + @Override + public int getRequiredAPIVersion() { + return 24; + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMk4ControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMk4ControllerExtension.java new file mode 100644 index 00000000..0b18d25b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkMk4ControllerExtension.java @@ -0,0 +1,253 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import com.bitwig.extension.controller.ControllerExtension; +import com.bitwig.extension.controller.ControllerExtensionDefinition; +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.DocumentState; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.akai.apc.common.control.ClickEncoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkCcAssignment; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkMultiStateButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.LineDisplay; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkMonoState; +import com.bitwig.extensions.controllers.akai.mpkmk4.layers.LayerCollection; +import com.bitwig.extensions.controllers.akai.mpkmk4.layers.LayerId; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.FocusMode; + +public class MpkMk4ControllerExtension extends ControllerExtension { + + private final String[] RECORD_QUANTIZE = {"OFF", "1/32", "1/16", "1/8", "1/4"}; + private static ControllerHost debugHost; + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + private ControllerHost host; + private final Variant variant; + private HardwareSurface surface; + + private Layer mainLayer; + private Layer topEncoderLayer; + + private MpkMidiProcessor midiProcessor; + private GlobalStates globalStates; + private FocusMode recordFocusMode = FocusMode.LAUNCHER; + + private String recQuant; + private boolean notifyQuant; + private int quantIndex = 2; + + private LineDisplay mainDisplay; + + 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)); + } + } + + public enum Variant { + MINI + } + + public MpkMk4ControllerExtension(final ControllerExtensionDefinition definition, final ControllerHost host, + final Variant variant) { + super(definition, host); + this.variant = variant; + } + + @Override + public void init() { + this.host = getHost(); + debugHost = host; + final Context diContext = new Context(this); + globalStates = new GlobalStates(this.variant); + diContext.registerService(GlobalStates.class, globalStates); + + final LayerCollection layerCollection = diContext.getService(LayerCollection.class); + + mainLayer = layerCollection.get(LayerId.MAIN); + topEncoderLayer = layerCollection.get(LayerId.OVER_LAYER); + surface = diContext.getService(HardwareSurface.class); + midiProcessor = diContext.getService(MpkMidiProcessor.class); + mainDisplay = diContext.getService(MpkHwElements.class).getMainLineDisplay(); + initTransport(diContext); + + midiProcessor.init(); + diContext.activate(); + } + + private void initTransport(final Context diContext) { + final Transport transport = diContext.getService(Transport.class); + final MpkHwElements hwElements = diContext.getService(MpkHwElements.class); + final MpkButton shiftButton = hwElements.getShiftButton(); + final LayerCollection layerCollection = diContext.getService(LayerCollection.class); + final Application application = diContext.getService(Application.class); + final DocumentState documentState = getHost().getDocumentState(); + final MpkFocusClip focusClip = diContext.getService(MpkFocusClip.class); + + application.recordQuantizationGrid().addValueObserver(this::handleRecordQuant); + 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)); + + transport.isClipLauncherAutomationWriteEnabled().markInterested(); + transport.isClipLauncherOverdubEnabled().markInterested(); + transport.isArrangerAutomationWriteEnabled().markInterested(); + transport.isArrangerOverdubEnabled().markInterested(); + transport.isArrangerRecordEnabled().markInterested(); + + final Layer shiftLayer = layerCollection.get(LayerId.SHIFT); + shiftButton.bindIsPressed(mainLayer, pressed -> handleShift(pressed, shiftLayer, hwElements)); + + final MpkMultiStateButton playButton = hwElements.getButton(MpkCcAssignment.PLAY); + playButton.bindLightDimmed(mainLayer, transport.isPlaying()); + playButton.bindPressed( + mainLayer, () -> { + final boolean wasRecording = transport.isArrangerRecordEnabled().get(); + transport.restart(); + if (wasRecording) { + transport.isArrangerRecordEnabled().set(true); + } + }); + playButton.bindLightDimmed(shiftLayer, transport.isPlaying()); + playButton.bindPressed(shiftLayer, transport.continuePlaybackAction()); + + final MpkMultiStateButton recordButton = hwElements.getButton(MpkCcAssignment.REC); + recordButton.bindLight(mainLayer, () -> recordButtonState(transport)); + recordButton.bindPressed(mainLayer, () -> handleRecordPressed(transport, focusClip)); + final MpkMultiStateButton loopButton = hwElements.getButton(MpkCcAssignment.LOOP); + loopButton.bindLightDimmed(mainLayer, transport.isArrangerLoopEnabled()); + loopButton.bindPressed(mainLayer, () -> transport.isArrangerLoopEnabled().toggle()); + + final MpkMultiStateButton overdubButton = hwElements.getButton(MpkCcAssignment.OVER); + overdubButton.bindLightDimmed(mainLayer, () -> isOverdubActive(transport)); + overdubButton.bindPressed(mainLayer, () -> handleOverdubPressed(transport)); + overdubButton.bindLightDimmed(shiftLayer, () -> isAutomationOverdubActive(transport)); + overdubButton.bindPressed(shiftLayer, () -> handleAutomationOverdubPressed(transport)); + + + final MpkMultiStateButton undoButton = hwElements.getButton(MpkCcAssignment.UNDO); + undoButton.bindLightDimmed(mainLayer, application.canUndo()); + undoButton.bindPressed(mainLayer, application.undoAction()); + undoButton.bindLightDimmed(shiftLayer, application.canRedo()); + undoButton.bindPressed(shiftLayer, application.redoAction()); + + final MpkMultiStateButton tempoButton = hwElements.getButton(MpkCcAssignment.TAP_TEMPO); + tempoButton.bindLightPressedOnDimmed(mainLayer); + tempoButton.bindPressed(mainLayer, transport.tapTempoAction()); + + tempoButton.bindLightOnOff(shiftLayer, transport.isMetronomeEnabled()); + tempoButton.bindPressed(shiftLayer, () -> transport.isMetronomeEnabled().toggle()); + + final ClickEncoder encoder = hwElements.getMainEncoder(); + encoder.bind(topEncoderLayer, inc -> incrementQuantize(inc, application.recordQuantizationGrid())); + recordButton.bindLight(shiftLayer, () -> "OFF".equals(recQuant) ? MpkMonoState.OFF : MpkMonoState.FULL_ON); + recordButton.bindIsPressed(shiftLayer, pressed -> toggleRecMode(pressed, application.recordQuantizationGrid())); + + } + + private void handleShift(final Boolean pressed, final Layer shiftLayer, final MpkHwElements hwElements) { + shiftLayer.setIsActive(pressed); + globalStates.getShiftHeld().set(pressed); + hwElements.applyShiftToEncoders(pressed); + if (!pressed) { + topEncoderLayer.setIsActive(false); + } + } + + private void incrementQuantize(final int inc, final SettableEnumValue quantValue) { + final int newIndex = Math.max(1, Math.min(RECORD_QUANTIZE.length - 1, quantIndex + inc)); + if (newIndex != quantIndex) { + quantIndex = newIndex; + quantValue.set(RECORD_QUANTIZE[quantIndex]); + notifyQuant = true; + } + } + + private void toggleRecMode(final boolean pressed, final SettableEnumValue quantValue) { + if (pressed) { + if ("OFF".equals(recQuant)) { + quantValue.set(RECORD_QUANTIZE[quantIndex]); + } else { + quantValue.set("OFF"); + } + mainDisplay.temporaryInfo(1, "Rec Quantize", recQuant); + notifyQuant = true; + } + topEncoderLayer.setIsActive(pressed); + } + + private void handleRecordQuant(final String recQuant) { + this.recQuant = recQuant.toUpperCase(); + if (notifyQuant) { + mainDisplay.temporaryInfo(1, "Rec Quantize", recQuant); + notifyQuant = false; + } + + } + + private MpkMonoState recordButtonState(final Transport transport) { + if (recordFocusMode == FocusMode.LAUNCHER) { + return transport.isClipLauncherOverdubEnabled().get() ? MpkMonoState.FULL_ON : MpkMonoState.DIMMED; + } else { + return transport.isArrangerRecordEnabled().get() ? MpkMonoState.FULL_ON : MpkMonoState.DIMMED; + } + } + + private void handleRecordPressed(final Transport transport, final MpkFocusClip focusClip) { + if (recordFocusMode == FocusMode.LAUNCHER) { + focusClip.invokeRecord(); + } else { + transport.record(); + } + } + + private boolean isOverdubActive(final Transport transport) { + if (recordFocusMode == FocusMode.LAUNCHER) { + return transport.isClipLauncherOverdubEnabled().get(); + } else { + return transport.isArrangerOverdubEnabled().get(); + } + } + + private void handleOverdubPressed(final Transport transport) { + if (recordFocusMode == FocusMode.LAUNCHER) { + transport.isClipLauncherOverdubEnabled().toggle(); + } else { + transport.isArrangerOverdubEnabled().toggle(); + } + } + + private boolean isAutomationOverdubActive(final Transport transport) { + return transport.isArrangerAutomationWriteEnabled().get(); + } + + private void handleAutomationOverdubPressed(final Transport transport) { + // if (recordFocusMode == FocusMode.LAUNCHER) { + // transport.isClipLauncherAutomationWriteEnabled().toggle(); + // } + transport.isArrangerAutomationWriteEnabled().toggle(); + } + + + @Override + public void flush() { + surface.updateHardware(); + } + + @Override + public void exit() { + midiProcessor.exit(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkViewControl.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkViewControl.java new file mode 100644 index 00000000..dbac356c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/MpkViewControl.java @@ -0,0 +1,131 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4; + +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.CursorDeviceFollowMode; +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.PinnableCursorDevice; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkColor; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class MpkViewControl { + private final TrackBank trackBank; + private final CursorTrack cursorTrack; + private final Track rootTrack; + private final Clip cursorClip; + private int selectedTrackIndex; + private final MpkColor[] trackColors = new MpkColor[8]; + private final PinnableCursorDevice cursorDevice; + private final TrackBank focusTrackBank; + private final PinnableCursorDevice primaryDevice; + private final DrumPadBank focusDrumPad; + private final DrumPadBank padBank; + private int padBankScrollPosition; + private final Clip arrangerCursorClip; + + public MpkViewControl(final ControllerHost host) { + rootTrack = host.getProject().getRootTrackGroup(); + trackBank = host.createTrackBank(4, 2, 4, true); + focusTrackBank = host.createTrackBank(1, 1, 1); + cursorTrack = host.createCursorTrack(6, 128); + trackBank.followCursorTrack(cursorTrack); + cursorClip = host.createLauncherCursorClip(32, 128); + arrangerCursorClip = host.createArrangerCursorClip(32, 128); + + cursorClip.setStepSize(0.125); + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final int index = i; + final Track track = trackBank.getItemAt(i); + prepareTrack(track); + track.color().addValueObserver((r, g, b) -> { + trackColors[index] = MpkColor.getColor(r, g, b); + }); + track.addIsSelectedInMixerObserver(select -> { + if (select) { + this.selectedTrackIndex = index; + } + }); + } + + cursorDevice = cursorTrack.createCursorDevice(); + cursorDevice.hasNext().markInterested(); + cursorDevice.hasPrevious().markInterested(); + primaryDevice = + cursorTrack.createCursorDevice("DrumDetection", "Pad Device", 16, CursorDeviceFollowMode.FIRST_INSTRUMENT); + padBank = primaryDevice.createDrumPadBank(16); + for (int i = 0; i < padBank.getSizeOfBank(); i++) { + final int index = i; + final DrumPad pad = padBank.getItemAt(i); + pad.addIsSelectedInMixerObserver(selectedInMixer -> handleSelectedInMixer(index, selectedInMixer)); + } + padBank.scrollPosition().addValueObserver(scrollPosition -> this.padBankScrollPosition = scrollPosition); + focusDrumPad = primaryDevice.createDrumPadBank(1); + } + + private void handleSelectedInMixer(final int index, final boolean selectedInMixer) { + if (selectedInMixer) { + focusDrumPad.scrollPosition().set(padBankScrollPosition + index); + } + } + + private void prepareTrack(final Track track) { + track.arm().markInterested(); + track.exists().markInterested(); + track.solo().markInterested(); + track.mute().markInterested(); + track.isQueuedForStop().markInterested(); + track.isStopped().markInterested(); + } + + public DrumPadBank getPadBank() { + return padBank; + } + + public TrackBank getTrackBank() { + return trackBank; + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public TrackBank getFocusTrackBank() { + return focusTrackBank; + } + + public PinnableCursorDevice getCursorDevice() { + return cursorDevice; + } + + public PinnableCursorDevice getPrimaryDevice() { + return primaryDevice; + } + + public Track getRootTrack() { + return rootTrack; + } + + public int getSelectedTrackIndex() { + return selectedTrackIndex; + } + + public DrumPadBank getFocusDrumPad() { + return focusDrumPad; + } + + public Clip getCursorClip() { + return cursorClip; + } + + public void invokeArrangerQuantize() { + arrangerCursorClip.quantize(1.0); + final ClipLauncherSlot slot = arrangerCursorClip.clipLauncherSlot(); + slot.showInEditor(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/ScaleSetup.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/ScaleSetup.java new file mode 100644 index 00000000..91138d92 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/ScaleSetup.java @@ -0,0 +1,106 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4; + +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extensions.framework.values.IntValueObject; +import com.bitwig.extensions.framework.values.Scale; +import com.bitwig.extensions.framework.values.ValueObject; + +public class ScaleSetup { + private final static String[] NOTES = + {" C", " C#", " D", " D#", " E", " F", " F#", " G", " G#", " A", " A#", " B"}; + private final ValueObject scale = + new ValueObject<>(Scale.CHROMATIC, ScaleSetup::increment, ScaleSetup::convert); + private final IntValueObject baseNote = new IntValueObject(0, 0, 11, v -> NOTES[v]); + private final IntValueObject octaveOffset = new IntValueObject(4, 0, 8); + private final List changeListeners = new ArrayList<>(); + + public ScaleSetup() { + baseNote.addValueObserver(v -> fireChange()); + octaveOffset.addValueObserver(v -> fireChange()); + scale.addValueObserver(v -> fireChange()); + } + + public IntValueObject getBaseNote() { + return baseNote; + } + + public IntValueObject getOctaveOffset() { + return octaveOffset; + } + + public ValueObject getScale() { + return scale; + } + + public String getScaleInfo() { + return "%s %s".formatted(NOTES[baseNote.get()], scale.get().getName()); + } + + private void fireChange() { + changeListeners.forEach(l -> l.run()); + } + + public static String toNote(final int noteValue) { + return NOTES[noteValue % 12]; + } + + public void addChangeListener(final Runnable action) { + changeListeners.add(action); + } + + public boolean canIncrementScale(final int dir) { + if (dir < 0 == scale.get().ordinal() > 0) { + return true; + } else { + return dir > 0 && scale.get().ordinal() + dir < Scale.values().length; + } + } + + private static Scale increment(final Scale current, final int amount) { + final int ord = current.ordinal(); + final Scale[] values = Scale.values(); + final int newOrd = ord + amount; + if (newOrd < 0) { + return values[0]; + } + if (newOrd >= values.length) { + return values[values.length - 1]; + } + return values[newOrd]; + } + + private static String convert(final Scale scale) { + return scale.getName(); + } + + public List getNoteSequence(final int len) { + final List result = new ArrayList<>(); + final int[] intervals = scale.get().getIntervals(); + for (int i = 0; i < len; i++) { + final int octave = octaveOffset.get() + i / intervals.length; + final int note = baseNote.get() + octave * 12 + intervals[i % intervals.length]; + if (note < 128) { + result.add(note); + } + } + return result; + } + + public boolean[] getBaseNotes(final int len) { + final boolean[] result = new boolean[len]; + final int[] intervals = scale.get().getIntervals(); + for (int i = 0; i < len; i++) { + if (i % intervals.length == 0) { + result[i] = true; + } + } + + return result; + } + + public String getStartInfo() { + return "%s(%d)".formatted(octaveOffset.get(), octaveOffset.get() * 12 + baseNote.get()); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/bindings/ParameterDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/bindings/ParameterDisplayBinding.java new file mode 100644 index 00000000..486ced01 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/bindings/ParameterDisplayBinding.java @@ -0,0 +1,62 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.bindings; + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.Encoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.ParameterValues; +import com.bitwig.extensions.framework.Binding; + +public class ParameterDisplayBinding extends Binding { + + private final ParameterValues paramValues; + private final int index; + private String name = ""; + private String displayValue = ""; + + public ParameterDisplayBinding(final Parameter source, final Encoder encoder, final ParameterValues target, + final int index) { + super(source, source, target); + this.paramValues = target; + this.index = index; + encoder.getEncoder().isUpdatingTargetValue().addValueObserver(this::handleIsUpdating); + source.value().markInterested(); + source.value().displayedValue().addValueObserver(this::handleDisplayValue); + source.name().addValueObserver(this::handleName); + this.displayValue = source.displayedValue().get(); + this.name = source.name().get(); + } + + private void handleName(final String name) { + this.name = name; + paramValues.setNames(index, name); + invokeUpdate(); + } + + private void handleDisplayValue(final String value) { + this.displayValue = value; + paramValues.setValue(index, displayValue); + invokeUpdate(); + } + + private void handleIsUpdating(final boolean updating) { + if (updating) { + invokeUpdate(); + } + } + + private void invokeUpdate() { + if (isActive()) { + paramValues.update(); + } + } + + @Override + protected void deactivate() { + } + + @Override + protected void activate() { + } + + public void update() { + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/Encoder.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/Encoder.java new file mode 100644 index 00000000..5171bd74 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/Encoder.java @@ -0,0 +1,36 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.controls; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extensions.framework.Layer; + +public class Encoder { + private final RelativeHardwareKnob encoder; + + public Encoder(final int index, final int ccNr, final HardwareSurface surface, final MidiIn midiIn) { + encoder = surface.createRelativeHardwareKnob("ENCODER_" + (index + 1)); + encoder.setAdjustValueMatcher(midiIn.createRelative2sComplementCCValueMatcher(0, ccNr, 200)); + encoder.setStepSize(0.1); + } + + public void setStepSize(final double value) { + encoder.setStepSize(value); + } + + public void bindParameter(final Layer layer, final Parameter parameter) { + final RelativeValueBinding binding = new RelativeValueBinding(encoder, parameter); + layer.addBinding(binding); + } + + public void bindValue(final Layer layer, final SettableRangedValue value) { + final RelativeValueBinding binding = new RelativeValueBinding(encoder, value); + layer.addBinding(binding); + } + + public RelativeHardwareKnob getEncoder() { + return encoder; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkButton.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkButton.java new file mode 100644 index 00000000..200bcc2b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkButton.java @@ -0,0 +1,124 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.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.MidiIn; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.time.TimeRepeatEvent; +import com.bitwig.extensions.framework.time.TimedDelayEvent; +import com.bitwig.extensions.framework.time.TimedEvent; + +public class MpkButton { + public static final int STD_REPEAT_DELAY = 600; + public static final int STD_REPEAT_FREQUENCY = 200; + + protected HardwareButton hwButton; + protected MpkMidiProcessor midiProcessor; + private TimedEvent currentTimer; + protected final int midiId; + protected final int channel; + + public MpkButton(final int channel, final int midiId, final boolean isCc, final String name, + final HardwareSurface surface, final MpkMidiProcessor midiProcessor) { + this.midiId = midiId; + this.channel = channel; + this.midiProcessor = midiProcessor; + final MidiIn midiIn = midiProcessor.getDawMidiIn(); + hwButton = surface.createHardwareButton(name); + if (isCc) { + hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, midiId, 0x7F)); + hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, midiId, 0x00)); + } else { + hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(channel, midiId)); + hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, midiId)); + } + hwButton.isPressed().markInterested(); + } + + public void forceUpdate() { + + } + + 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 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); + } + + + /** + * Models following behavior. Pressing and Releasing the button within the given delay time executes the click + * event. + * Long Pressing the button invokes the holdAction with true and then the same action with false once released. + * + * @param layer the layer + * @param clickAction the action invoked if the button is pressed and release in less than the given delay time + * @param holdAction action called with true when the delay time expires and with false if released under this + * condition + * @param delayTime the delay time + */ + public void bindDelayedHold(final Layer layer, final Runnable clickAction, final Consumer holdAction, + final long delayTime) { + layer.bind(hwButton, hwButton.pressedAction(), () -> initiateHold(holdAction, delayTime)); + layer.bind(hwButton, hwButton.releasedAction(), () -> handleDelayedRelease(clickAction, holdAction)); + } + + private void initiateHold(final Consumer holdAction, final long delayTime) { + currentTimer = new TimedDelayEvent(() -> holdAction.accept(true), delayTime); + midiProcessor.queueEvent(currentTimer); + } + + private void handleDelayedRelease(final Runnable clickAction, final Consumer holdAction) { + if (currentTimer != null && !currentTimer.isCompleted()) { + currentTimer.cancel(); + clickAction.run(); + currentTimer = null; + } else { + holdAction.accept(false); + } + } + + /** + * 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); + } + + public 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/akai/mpkmk4/controls/MpkCcAssignment.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkCcAssignment.java new file mode 100644 index 00000000..420bca83 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkCcAssignment.java @@ -0,0 +1,25 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.controls; + +public enum MpkCcAssignment { + + PLAY(0x4C), // + REC(0x4D), // + OVER(0x4E), // + LOOP(0x4A), // + UNDO(0x49), // + BANK_LEFT(0x50), // + BANK_RIGHT(0x51), // + BANK_AB(0xC), // + TAP_TEMPO(0x52), // + NOTE_REPEAT(0x52); + + private final int ccNr; + + MpkCcAssignment(final int ccNr) { + this.ccNr = ccNr; + } + + public int getCcNr() { + return ccNr; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkMultiStateButton.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkMultiStateButton.java new file mode 100644 index 00000000..5b41841d --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkMultiStateButton.java @@ -0,0 +1,92 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.controls; + +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.bitwig.extension.controller.api.BooleanValue; +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.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkColor; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkMonoState; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.values.Midi; + +public class MpkMultiStateButton extends MpkButton { + + protected MultiStateHardwareLight light; + + public MpkMultiStateButton(final int channel, final int midiId, final boolean isCc, final String name, + final HardwareSurface surface, final MpkMidiProcessor midiProcessor) { + super(channel, midiId, isCc, name, surface, midiProcessor); + light = surface.createMultiStateHardwareLight(name + "_LIGHT"); + light.state().onUpdateHardware(this::updateState); + light.state().setValue(MpkColor.OFF); + hwButton.setBackgroundLight(light); + } + + private void updateState(final InternalHardwareLightState internalHardwareLightState) { + if (internalHardwareLightState instanceof final MpkColor state) { + midiProcessor.sendMidi(Midi.NOTE_ON | state.getState(), midiId, state.getColorIndex()); + } else if (internalHardwareLightState instanceof final MpkMonoState state) { + midiProcessor.sendMidi(Midi.NOTE_ON | state.getState(), midiId, state.isOn() ? 0x7F : 0x00); + } else { + midiProcessor.sendMidi(Midi.NOTE_ON, midiId, 0); + } + } + + @Override + public void forceUpdate() { + updateState(light.state().currentValue()); + } + + public void bindLightPressedOnOff(final Layer layer) { + layer.bindLightState(() -> hwButton.isPressed().get() ? MpkMonoState.FULL_ON : MpkMonoState.OFF, light); + } + + public void bindLightPressedOnDimmed(final Layer layer) { + layer.bindLightState(() -> hwButton.isPressed().get() ? MpkMonoState.FULL_ON : MpkMonoState.DIMMED, light); + } + + public void bindLight(final Layer layer, final Supplier supplier) { + layer.bindLightState(supplier, light); + } + + public void bindLightDimmed(final Layer layer, final BooleanSupplier state) { + layer.bindLightState(() -> state.getAsBoolean() ? MpkMonoState.FULL_ON : MpkMonoState.DIMMED, light); + } + + public void bindLightDimmed(final Layer layer, final BooleanValue value) { + value.markInterested(); + layer.bindLightState(() -> value.get() ? MpkMonoState.FULL_ON : MpkMonoState.DIMMED, light); + } + + public void bindLightOff(final Layer layer) { + layer.bindLightState(() -> MpkMonoState.OFF, light); + } + + public void bindLightOnOff(final Layer layer, final BooleanSupplier state) { + layer.bindLightState(() -> state.getAsBoolean() ? MpkMonoState.FULL_ON : MpkMonoState.OFF, light); + } + + public void bindLightOnOff(final Layer layer, final BooleanValue value) { + value.markInterested(); + layer.bindLightState(() -> value.get() ? MpkMonoState.FULL_ON : MpkMonoState.OFF, light); + } + + public void bindLightPressed(final Layer layer, final Function supplier) { + layer.bindLightState(() -> supplier.apply(hwButton.isPressed().get()), light); + } + + public void bindLight(final Layer layer, final Function pressedCombine) { + layer.bindLightState(() -> pressedCombine.apply(hwButton.isPressed().get()), light); + } + + public void bindLightPressed(final Layer layer, final InternalHardwareLightState state, + final InternalHardwareLightState holdState) { + layer.bindLightState(() -> hwButton.isPressed().get() ? holdState : state, light); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkOnOffButton.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkOnOffButton.java new file mode 100644 index 00000000..d44eda7b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/MpkOnOffButton.java @@ -0,0 +1,48 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.controls; + +import java.util.function.BooleanSupplier; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.OnOffHardwareLight; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.values.Midi; + +public class MpkOnOffButton extends MpkButton { + private final OnOffHardwareLight light; + public static final int STD_REPEAT_DELAY = 400; + public static final int STD_REPEAT_FREQUENCY = 50; + + + public MpkOnOffButton(final int channel, final int midiId, final String name, final HardwareSurface surface, + final MpkMidiProcessor midiProcessor) { + super(channel, midiId, true, name, surface, midiProcessor); + light = surface.createOnOffHardwareLight(name + "_LIGHT"); + light.onUpdateHardware(() -> updateState(light.isOn().currentValue())); + hwButton.setBackgroundLight(light); + } + + private void updateState(final boolean on) { + if (on) { + midiProcessor.sendMidi(Midi.NOTE_ON | 6, midiId, 0x7F); + } else { + midiProcessor.sendMidi(Midi.NOTE_ON | 0, midiId, 0x7f); + } + } + + @Override + public void forceUpdate() { + updateState(light.isOn().currentValue()); + } + + public void bindLight(final Layer layer, final BooleanSupplier state) { + layer.bind(state, light); + } + + public void bindLightPressed(final Layer layer) { + hwButton.isPressed().markInterested(); + layer.bind(hwButton.isPressed(), light); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/RelativeValueBinding.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/RelativeValueBinding.java new file mode 100644 index 00000000..503df990 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/controls/RelativeValueBinding.java @@ -0,0 +1,47 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.controls; + +import com.bitwig.extension.controller.api.HardwareBinding; +import com.bitwig.extension.controller.api.RelativeHardwareControlBinding; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extensions.framework.Binding; + +public class RelativeValueBinding extends Binding { + + private HardwareBinding hwBinding; + + public RelativeValueBinding(final RelativeHardwareKnob source, final SettableRangedValue target) { + super(source, source, target); + } + + protected RelativeHardwareControlBinding getHardwareBinding() { + return getTarget().addBinding(getSource()); + } + + public void reset() { + if (!isActive()) { + return; + } + if (hwBinding != null) { + hwBinding.removeBinding(); + } + hwBinding = getHardwareBinding(); + } + + @Override + protected void deactivate() { + if (hwBinding != null) { + hwBinding.removeBinding(); + hwBinding = null; + } + } + + @Override + protected void activate() { + if (hwBinding != null) { + hwBinding.removeBinding(); + } + hwBinding = getHardwareBinding(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/LineDisplay.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/LineDisplay.java new file mode 100644 index 00000000..fa725494 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/LineDisplay.java @@ -0,0 +1,134 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import com.bitwig.extension.api.Color; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.framework.time.TimedDelayEvent; + +public class LineDisplay { + + private final MpkMidiProcessor midiProcessor; + private int currentLayer; + + private TimedDelayEvent fallbackEvent = null; + + private static class Line { + private String text = ""; + private int colorIndex = 0; + private MpkDisplayFont fontStyle = MpkDisplayFont.PT24; + } + + private final Line[] lines; + private boolean active; + + public LineDisplay(final MpkMidiProcessor midiProcessor, final MpkDisplayFont fontStyle, final int lines) { + this.midiProcessor = midiProcessor; + this.lines = new Line[lines]; + for (int i = 0; i < lines; i++) { + this.lines[i] = new Line(); + this.lines[i].fontStyle = fontStyle; + } + } + + public void setText(final int rowIndex, final String text, final int colorIndex) { + this.lines[rowIndex].text = text; + this.lines[rowIndex].colorIndex = colorIndex; + if (active) { + midiProcessor.setText(rowIndex, this.lines[rowIndex].fontStyle, text); + midiProcessor.setDisplayColor(rowIndex, colorIndex); + } + } + + public void setText(final int rowIndex, final String text) { + this.lines[rowIndex].text = text; + if (active) { + midiProcessor.setText(rowIndex, this.lines[rowIndex].fontStyle, text); + } + } + + public void setMenuLine(final int rowIndex, final MpkDisplayFont font, final int justification, + final Color foreGround, final Color background) { + if (active) { + midiProcessor.configureLine(font, rowIndex, justification, foreGround, background); + } + } + + public void setText(final int rowIndex, final String text, final MpkDisplayFont font, final int colorIndex) { + this.lines[rowIndex].text = text; + this.lines[rowIndex].fontStyle = font; + this.lines[rowIndex].colorIndex = colorIndex; + if (active) { + midiProcessor.setText(rowIndex, this.lines[rowIndex].fontStyle, text); + midiProcessor.setDisplayColor(rowIndex, colorIndex); + } + } + + public void setText(final int layer, final int rowIndex, final String text) { + if (layer == 0) { + this.lines[rowIndex].text = text; + } else if (layer != currentLayer) { + return; + } + if (active) { + midiProcessor.setText(rowIndex, this.lines[rowIndex].fontStyle, text); + } + } + + public void setColorIndex(final int layer, final int index, final int colorIndex) { + if (layer == 0) { + this.lines[index].colorIndex = colorIndex; + } else if (layer != currentLayer) { + return; + } + if (active) { + midiProcessor.setDisplayColor(index, colorIndex); + } + } + + public void setActive(final boolean active) { + this.active = active; + if (active) { + updateCurrent(); + } + } + + public void updateCurrent() { + for (int i = 0; i < lines.length; i++) { + midiProcessor.setDisplayColor(i, this.lines[i].colorIndex); + midiProcessor.setText(i, this.lines[i].fontStyle, this.lines[i].text); + } + } + + public boolean isActive() { + return active; + } + + public void activateTemporary(final int layer) { + this.currentLayer = layer; + if (fallbackEvent != null) { + fallbackEvent.cancel(); + } + fallbackEvent = new TimedDelayEvent(() -> resetLayer(), 2000); + midiProcessor.queueEvent(fallbackEvent); + } + + private void resetLayer() { + this.currentLayer = 0; + this.updateCurrent(); + this.fallbackEvent = null; + } + + public void temporaryInfo(final int layerIndex, final String line1, final String line2) { + activateTemporary(layerIndex); + showTemporary(layerIndex, line1, line2); + } + + public void showTemporary(final int layerIndex, final String line1, final String line2) { + if (currentLayer == layerIndex) { + setText(layerIndex, 1, line1); + setColorIndex(layerIndex, 1, 0); + setText(layerIndex, 2, line2); + setColorIndex(layerIndex, 2, 0); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MenuEntry.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MenuEntry.java new file mode 100644 index 00000000..4a4933b7 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MenuEntry.java @@ -0,0 +1,47 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.StringValue; + +public class MenuEntry { + private final int index; + private final String title; + private final StringValue value; + private IntConsumer incrementHandler; + private Runnable clickHandler; + + public MenuEntry(final int index, final String title, final StringValue value, final IntConsumer incrementHandler) { + this.index = index; + this.title = title; + this.value = value; + this.incrementHandler = incrementHandler; + } + + public MenuEntry(final int index, final String title, final Runnable clickHandler) { + this.index = index; + this.title = title; + this.value = null; + this.clickHandler = clickHandler; + } + + public int getIndex() { + return index; + } + + public String getTitle() { + return title; + } + + public StringValue getValue() { + return value; + } + + public IntConsumer getIncrementHandler() { + return incrementHandler; + } + + public Runnable getClickHandler() { + return clickHandler; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MenuList.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MenuList.java new file mode 100644 index 00000000..b45714c3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MenuList.java @@ -0,0 +1,87 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntConsumer; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.StringValue; + +public class MenuList { + private static final Color SELECT_BG = Color.fromRGB255(0 << 3, 10 << 2, 31 << 3); + private static final Color SELECT_BG_PARAM = Color.fromRGB255(0 << 3, 0, 0x15 << 3); + private static final Color SELECT_FG = Color.fromRGB255(255, 255, 255); + public static final Color BACKGROUND = Color.fromRGB255(255, 255, 220); + public static final Color FOREGROUND = Color.fromRGB255(0, 0, 0); + + private final List entries = new ArrayList<>(); + private int scrollPos = 0; + private boolean valueFocus = false; + + public void add(final String title, final Runnable clickHandler) { + this.entries.add(new MenuEntry(entries.size(), title, clickHandler)); + } + + public void add(final String title, final StringValue value, final IntConsumer incrementHandler) { + this.entries.add(new MenuEntry(entries.size(), title, value, incrementHandler)); + } + + public MenuEntry get(final int index) { + return entries.get(index); + } + + public boolean increment(final int inc) { + final int nextEntry = scrollPos + inc; + if (nextEntry >= 0 && nextEntry < entries.size()) { + scrollPos = nextEntry; + return true; + } + return false; + } + + public MenuEntry getCurrent() { + return entries.get(scrollPos); + } + + public void updateDisplay(final LineDisplay display) { + final int scrollOffset = Math.max(0, scrollPos - 2); + for (int i = 0; i < 3; i++) { + final int index = i + scrollOffset; + if (index < entries.size()) { + updateMenuEntry(entries.get(index), display); + } + } + } + + public void updateMenuEntry(final MenuEntry entry, final LineDisplay menuDisplay) { + final int i = entry.getIndex() - Math.max(0, scrollPos - 2); + final String value = entry.getValue() != null ? entry.getValue().get() : ""; + if (entry.getIndex() == scrollPos) { + menuDisplay.setMenuLine(i, MpkDisplayFont.PT24, 0, SELECT_FG, valueFocus ? SELECT_BG_PARAM : SELECT_BG); + if (entry.getValue() == null) { + menuDisplay.setText(i, "%s".formatted(entry.getTitle())); + } else { + menuDisplay.setText(i, "%s%s: %s".formatted(valueFocus ? ">" : "", entry.getTitle(), value)); + } + } else { + menuDisplay.setMenuLine(i, MpkDisplayFont.PT24, 0, FOREGROUND, BACKGROUND); + if (entry.getTitle() == null) { + menuDisplay.setText(i, "%s".formatted(entry.getTitle())); + } else { + menuDisplay.setText(i, "%s: %s".formatted(entry.getTitle(), value)); + } + } + } + + public void toggleValueFocus() { + this.valueFocus = !valueFocus; + } + + public void resetValueFocus() { + this.valueFocus = false; + } + + public boolean onValue() { + return valueFocus; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkColor.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkColor.java new file mode 100644 index 00000000..b883179e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkColor.java @@ -0,0 +1,85 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import java.util.HashMap; +import java.util.Map; + +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; + +public class MpkColor extends InternalHardwareLightState { + private static final Map indexLookup = new HashMap<>(); + + public static final MpkColor OFF = new MpkColor(0); + public static final MpkColor WHITE = new MpkColor(3); + public static final MpkColor GRAY = new MpkColor(2); + public static final MpkColor YELLOW = new MpkColor(13); + public static final MpkColor RED = new MpkColor(5); + public static final MpkColor ORANGE = new MpkColor(9); + public static final MpkColor BLUE = new MpkColor(67); + public static final MpkColor GREEN = new MpkColor(21); + + private final int colorIndex; + private final int state; + private final MpkColor[] stateVariants = new MpkColor[16]; + + private MpkColor(final int colorIndex, final int state) { + this.colorIndex = colorIndex; + this.state = state; + } + + private MpkColor(final int colorIndex) { + this(colorIndex, MpkMonoState.SOLID_STATE); + this.stateVariants[MpkMonoState.SOLID_STATE] = this; + } + + public static MpkColor getColor(final float red, final float green, final float blue) { + final int rv = (int) Math.floor(red * 255); + final int gv = (int) Math.floor(green * 255); + final int bv = (int) Math.floor(blue * 255); + final int colorLookup = rv << 16 | gv << 8 | bv; + + + final MpkColor idx = indexLookup.computeIfAbsent( + colorLookup, index -> { + final int ci = MpkColorLookup.rgbToIndex(red, green, blue); + return new MpkColor(ci, MpkMonoState.SOLID_STATE); + }); + //MpkMk4ControllerExtension.println(" > %d %d %d = %d", rv, gv, bv, idx.colorIndex); + return idx; + } + + public MpkColor variant(final int state) { + if (this.stateVariants[state] == null) { + this.stateVariants[state] = new MpkColor(this.colorIndex, state); + } + return this.stateVariants[state]; + } + + public int getState() { + return state; + } + + public int getColorIndex() { + return colorIndex; + } + + @Override + public HardwareLightVisualState getVisualState() { + return null; + } + + @Override + public final boolean equals(final Object o) { + if (!(o instanceof final MpkColor mpkColor)) { + return false; + } + return colorIndex == mpkColor.colorIndex && state == mpkColor.state; + } + + @Override + public int hashCode() { + int result = colorIndex; + result = 31 * result + state; + return result; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkColorLookup.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkColorLookup.java new file mode 100644 index 00000000..0b0e60aa --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkColorLookup.java @@ -0,0 +1,135 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import java.util.List; + +import com.bitwig.extension.api.Color; + +public class MpkColorLookup { + + private static final List COLORS = List.of( + new ColorMatch(0, 0x00, 0x00, 0x00), // + new ColorMatch(2, 0x7F, 0x7F, 0x7F), // + new ColorMatch(3, 0xFF, 0xFF, 0xFF), // + new ColorMatch(4, 0xFF, 0x4C, 0x4C), // + new ColorMatch(5, 0xFF, 0x00, 0x00), // + new ColorMatch(8, 0xFF, 0xBD, 0x6C), // + new ColorMatch(9, 0xFF, 0x54, 0x00), // + new ColorMatch(12, 0xFF, 0xFF, 0x4C), // + new ColorMatch(13, 0xFF, 0xFF, 0x00), // + new ColorMatch(16, 0x88, 0xFF, 0x4C), // + new ColorMatch(17, 0x54, 0xFF, 0x00), // + new ColorMatch(18, 0x1D, 0x59, 0x00), // + new ColorMatch(20, 0x4C, 0xFF, 0x4C), // + new ColorMatch(21, 0x00, 0xFF, 0x00), // + new ColorMatch(22, 0x00, 0x59, 0x00), // + new ColorMatch(24, 0x4C, 0xFF, 0x5E), // + new ColorMatch(25, 0x00, 0xFF, 0x19), // + new ColorMatch(26, 0x00, 0x59, 0x0D), // + new ColorMatch(28, 0x4C, 0xFF, 0x88), // + new ColorMatch(29, 0x00, 0xFF, 0x55), // + new ColorMatch(30, 0x00, 0x59, 0x1D), // + + new ColorMatch(32, 0x4C, 0xFF, 0xB7), // + new ColorMatch(33, 0x00, 0xFF, 0x99), // + new ColorMatch(34, 0x00, 0x59, 0x35), // + new ColorMatch(36, 0x4C, 0xC3, 0xFF), // + new ColorMatch(37, 0x00, 0xA9, 0xFF), // + new ColorMatch(38, 0x00, 0x41, 0x52), // + new ColorMatch(40, 0x4C, 0x88, 0xFF), // + new ColorMatch(41, 0x00, 0x55, 0xFF), // + new ColorMatch(44, 0x4C, 0x4C, 0xFF), // + new ColorMatch(45, 0x00, 0x00, 0xFF), // + new ColorMatch(48, 0x87, 0x4C, 0xFF), // + new ColorMatch(49, 0x54, 0x00, 0xFF), // + new ColorMatch(52, 0xFF, 0x4C, 0xFF), // + new ColorMatch(53, 0xFF, 0x00, 0xFF), // + new ColorMatch(56, 0xFF, 0x4C, 0x87), // + new ColorMatch(57, 0xFF, 0x00, 0x54), // + + new ColorMatch(60, 0xFF, 0x15, 0x00), // + new ColorMatch(61, 0x99, 0x35, 0x00), // + new ColorMatch(62, 0x79, 0x51, 0x00), // + new ColorMatch(63, 0x43, 0x64, 0x00), // + new ColorMatch(64, 0x03, 0x39, 0x00), // + new ColorMatch(65, 0x00, 0x57, 0x35), // + new ColorMatch(66, 0x00, 0x54, 0x7F), // + new ColorMatch(67, 0x00, 0x00, 0xFF), // + new ColorMatch(68, 0x00, 0x45, 0x4F), // + new ColorMatch(69, 0x25, 0x00, 0xCC), // + + new ColorMatch(73, 0xBD, 0xFF, 0x2D), // + new ColorMatch(74, 0xAF, 0xED, 0x06), // + new ColorMatch(75, 0x64, 0xFF, 0x09), // + new ColorMatch(76, 0x10, 0x8B, 0x00), // + new ColorMatch(77, 0x00, 0xFF, 0x87), // + new ColorMatch(79, 0x00, 0x2A, 0xFF), // + new ColorMatch(80, 0x3F, 0x00, 0xFF), // + new ColorMatch(81, 0x7A, 0x00, 0xFF), // + new ColorMatch(82, 0xB2, 0x1A, 0x7D), // + new ColorMatch(84, 0xFF, 0x4A, 0x00), // + new ColorMatch(85, 0x88, 0xE1, 0x06), // + new ColorMatch(86, 0x72, 0xFF, 0x15), // + new ColorMatch(88, 0x3B, 0xFF, 0x26), // + new ColorMatch(90, 0x38, 0xFF, 0xCC), // + new ColorMatch(91, 0x5B, 0x8A, 0xFF), // + new ColorMatch(92, 0x31, 0x51, 0xC6), // + new ColorMatch(93, 0x87, 0x7F, 0xE9), // + new ColorMatch(94, 0xD3, 0x1D, 0xFF), // + new ColorMatch(95, 0xFF, 0x00, 0x5D), // + + new ColorMatch(96, 0xFF, 0x7F, 0x00), // + new ColorMatch(97, 0xB9, 0xB0, 0x00), // + new ColorMatch(98, 0x90, 0xFF, 0x00), // + new ColorMatch(101, 0x14, 0x4C, 0x10), // + new ColorMatch(102, 0x0D, 0x50, 0x38), // + new ColorMatch(106, 0xA8, 0x00, 0x0A), // + new ColorMatch(107, 0xDE, 0x51, 0x3D), // + new ColorMatch(108, 0xD8, 0x6A, 0x1C), // + new ColorMatch(109, 0xFF, 0xE1, 0x26), // + new ColorMatch(110, 0x9E, 0xE1, 0x2F), // + new ColorMatch(111, 0x67, 0xB5, 0x0F), // + new ColorMatch(113, 0xDC, 0xFF, 0x6B), // + new ColorMatch(114, 0x80, 0xFF, 0xBD), // + new ColorMatch(116, 0x8E, 0x66, 0xFF), // + new ColorMatch(118, 0x75, 0x75, 0x75), // + new ColorMatch(119, 0xE0, 0xFF, 0xFF), // + new ColorMatch(120, 0xA0, 0x00, 0x00), // + new ColorMatch(122, 0x1A, 0xD0, 0x00), // + new ColorMatch(126, 0xB3, 0x5F, 0x00) // + ); + + 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)); + } + + public boolean isGrayTone() { + return red == green && green == blue; + } + + private double colorDistance(final float r1, final float g1, final float b1) { + final double dr = r1 - (this.red / 255.0f); + final double dg = g1 - (this.green / 255.0f); + final double db = b1 - (this.blue / 255.0f); + return 0.15f * dr * dr + 0.35f * dg * dg + 0.10f * db * db; + //return dr * dr + dg * dg + db * db; // Euclidean distance + } + } + + public static int rgbToIndex(final float red, final float green, final float blue) { + int closestIndex = -1; + double minDistance = Double.MAX_VALUE; + + for (int i = 0; i < COLORS.size(); i++) { + final ColorMatch c = COLORS.get(i); + final double distance = c.colorDistance(red, green, blue); + + if (distance < minDistance) { + minDistance = distance; + closestIndex = i; + } + } + final ColorMatch colorMatch = COLORS.get(closestIndex); + return colorMatch.index(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkDisplayFont.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkDisplayFont.java new file mode 100644 index 00000000..9fe41506 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkDisplayFont.java @@ -0,0 +1,17 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +public enum MpkDisplayFont { + PT16(0x0), + PT16_BOLD(0x1), + PT24(0x2), + PT24_BOLD(0x4); + private final int value; + + MpkDisplayFont(final int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkMonoState.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkMonoState.java new file mode 100644 index 00000000..80371d8c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/MpkMonoState.java @@ -0,0 +1,64 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; + +public class MpkMonoState extends InternalHardwareLightState { + public static final int SOLID_10 = 0; + public static final int SOLID_25 = 1; + public static final int SOLID_50 = 2; + public static final int SOLID_65 = 3; + public static final int SOLID_75 = 4; + public static final int SOLID_90 = 6; + public static final int SOLID_STATE = 6; + public static final int PULSE1_16 = 7; + public static final int PULSE1_8 = 8; + public static final int PULSE1_4 = 9; + public static final int PULSE1_2 = 10; + public static final int BLINK1_24 = 11; + public static final int BLINK1_16 = 12; + public static final int BLINK1_8 = 13; + public static final int BLINK1_4 = 14; + public static final int BLINK1_2 = 15; + + public static MpkMonoState FULL_ON = new MpkMonoState(6, true); + public static MpkMonoState OFF = new MpkMonoState(6, false); + public static MpkMonoState DIMMED = new MpkMonoState(0, true); + + private final int state; + private final boolean on; + + private MpkMonoState(final int state, final boolean on) { + this.state = state; + this.on = on; + } + + public boolean isOn() { + return on; + } + + public int getState() { + return state; + } + + @Override + public HardwareLightVisualState getVisualState() { + return null; + } + + @Override + public final boolean equals(final Object o) { + if (!(o instanceof final MpkMonoState that)) { + return false; + } + + return state == that.state && on == that.on; + } + + @Override + public int hashCode() { + int result = state; + result = 31 * result + Boolean.hashCode(on); + return result; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/ParameterValues.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/ParameterValues.java new file mode 100644 index 00000000..49e7d9ed --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/ParameterValues.java @@ -0,0 +1,58 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import java.util.Arrays; + +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; + +public class ParameterValues { + + private final MpkMidiProcessor midiProcessor; + private final String[] values = new String[8]; + private final String[] names = new String[8]; + private final byte[] data = new byte[136]; + + public ParameterValues(final MpkMidiProcessor midiProcessor) { + this.midiProcessor = midiProcessor; + Arrays.fill(this.data, (byte) 0x20); + data[0] = (byte) 0xF0; + data[1] = (byte) 0x47; + data[2] = (byte) 0x7F; + data[3] = (byte) 0x5D; + data[4] = (byte) 0x1D; + data[5] = (byte) 0x01; + data[6] = (byte) 0x00; + data[135] = (byte) 0xF7; + } + + public void setValue(final int index, final String value) { + values[index] = StringUtil.toAsciiDisplay(value, 8); + final String strVal = values[index]; + for (int i = 0; i < 8; i++) { + if (i < strVal.length()) { + data[index * 8 + 71 + i] = (byte) strVal.charAt(i); + } else { + data[index * 8 + 71 + i] = 0x20; + } + } + } + + public void setNames(final int index, final String name) { + final String strVal = StringUtil.toAsciiDisplay(name, 8); + names[index] = strVal; + for (int i = 0; i < 8; i++) { + if (i < strVal.length()) { + data[index * 8 + 7 + i] = (byte) strVal.charAt(i); + } else { + data[index * 8 + 7 + i] = 0x20; + } + } + } + + public byte[] getData() { + return data; + } + + public void update() { + midiProcessor.sendSysEx(data); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/RemotesDisplayControl.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/RemotesDisplayControl.java new file mode 100644 index 00000000..24a69511 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/RemotesDisplayControl.java @@ -0,0 +1,110 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extensions.framework.values.BasicStringValue; + +public class RemotesDisplayControl { + public static final String BLANK_STRING = "--"; + + private final StringValue deviceName; + + private boolean active = false; + protected final LineDisplay mainDisplay; + private final BasicStringValue pageName = new BasicStringValue(); + + private int pageCount; + private int pageIndex; + private String[] pageNames = new String[0]; + + public RemotesDisplayControl(final StringValue deviceNameValue, final LineDisplay lineDisplay, + final CursorRemoteControlsPage deviceRemotes) { + this.mainDisplay = lineDisplay; + this.deviceName = deviceNameValue; + deviceNameValue.addValueObserver(this::handleDeviceNameChanged); + deviceRemotes.selectedPageIndex().addValueObserver(this::handlePageIndex); + deviceRemotes.pageCount().addValueObserver(this::handlePageCount); + deviceRemotes.pageNames().addValueObserver(this::handlePageNames); + pageName.addValueObserver(this::handlePageNameChanged); + } + + public void setActive(final boolean active) { + this.active = active; + updateDisplay(); + } + + public void updateTemporary() { + //mainDisplay.activateTemporary(1); + mainDisplay.temporaryInfo(1, deviceName.get(), pageName.get().isBlank() ? BLANK_STRING : pageName.get()); + } + + public boolean isActive() { + return active; + } + + public void updateDisplay() { + if (isActive()) { + if (deviceName.get().isBlank()) { + mainDisplay.setText(0, 1, BLANK_STRING); + mainDisplay.setText(0, 2, BLANK_STRING); + } else { + mainDisplay.setText(0, 1, deviceName.get()); + mainDisplay.setText(0, 2, pageName.get().isBlank() ? BLANK_STRING : pageName.get()); + } + } + } + + private void handleDeviceNameChanged(final String name) { + if (isActive()) { + mainDisplay.setText(0, 1, name.isBlank() ? BLANK_STRING : name); + } else { + mainDisplay.showTemporary( + 1, name.isBlank() ? BLANK_STRING : name, pageName.get().isBlank() ? BLANK_STRING : pageName.get()); + } + } + + private void handlePageNameChanged(final String page) { + if (isActive()) { + mainDisplay.setText(0, 2, page); + } else { + mainDisplay.showTemporary(1, deviceName.get(), page.isBlank() ? BLANK_STRING : pageName.get()); + } + } + + private void handlePageNames(final String[] pageNames) { + this.pageNames = pageNames; + if (pageIndex < this.pageNames.length) { + pageName.set(this.pageNames[pageIndex]); + } else if (pageNames.length == 0) { + pageName.set(BLANK_STRING); + } + } + + private void handlePageCount(final int pageCount) { + if (pageCount == -1) { + pageName.set(BLANK_STRING); + return; + } + this.pageCount = pageCount; + } + + private void handlePageIndex(final int pageIndex) { + if (pageIndex == -1) { + pageName.set(BLANK_STRING); + return; + } + this.pageIndex = pageIndex; + if (pageIndex < this.pageNames.length) { + pageName.set(this.pageNames[pageIndex]); + } + } + + public boolean canScrollRight() { + return pageIndex < pageCount - 1; + } + + public boolean canScrollLeft() { + return pageIndex > 0; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/RowDisplay.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/RowDisplay.java new file mode 100644 index 00000000..a423eb5b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/RowDisplay.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +public class RowDisplay { + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/ScreenRowState.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/ScreenRowState.java new file mode 100644 index 00000000..7b58c703 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/ScreenRowState.java @@ -0,0 +1,16 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +public class ScreenRowState { + + private int index; + private MpkDisplayFont font; + private int justification; + + private int foregroundRed; + private int foregroundGreen; + private int foregroundBlue; + + private int backgroundRed; + private int backgroundGreen; + private int backgroundBlue; +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/StringUtil.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/StringUtil.java new file mode 100644 index 00000000..08b6b433 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/display/StringUtil.java @@ -0,0 +1,73 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.display; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class StringUtil { + private static final char[] SPECIALS = { + 'ä', 'ü', 'ö', 'Ä', 'Ü', 'Ö', 'ß', 'é', 'è', 'ê', 'â', 'á', 'à', // + 'û', 'ú', 'ù', 'ô', 'ó', 'ò' + }; + private static final String[] REPLACE = { + "a", "u", "o", "A", "U", "O", "ss", "e", "e", "e", "a", "a", "a", // + "u", "u", "u", "o", "o", "o" + }; + + public static String toAsciiDisplayFill(final String name, final int len) { + final StringBuilder b = new StringBuilder(); + for (int i = 0; i < name.length() && b.length() < len; i++) { + final char c = name.charAt(i); + // if (c == 32) { + // continue; + // } + if (c < 128) { + b.append(c); + } else { + final int replacement = getReplace(c); + if (replacement >= 0) { + b.append(REPLACE[replacement]); + } + } + } + return b.toString(); + } + + + public static String toAsciiDisplay(final String name, final int maxLen) { + final StringBuilder b = new StringBuilder(); + for (int i = 0; i < name.length() && b.length() < maxLen; i++) { + final char c = name.charAt(i); + // if (c == 32) { + // continue; + // } + if (c < 128) { + b.append(c); + } else { + final int replacement = getReplace(c); + if (replacement >= 0) { + b.append(REPLACE[replacement]); + } + } + } + return b.toString(); + } + + private static int getReplace(final char c) { + for (int i = 0; i < SPECIALS.length; i++) { + if (c == SPECIALS[i]) { + return i; + } + } + return -1; + } + + public static String sysExString(byte[] data) { + List dataString = new ArrayList<>(); + for(byte b : data) { + dataString.add("%02X".formatted(b)); + } + return dataString.stream().collect(Collectors.joining(" ")); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/ClipLauncher.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/ClipLauncher.java new file mode 100644 index 00000000..d90b9774 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/ClipLauncher.java @@ -0,0 +1,119 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import java.util.List; + +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.SceneBank; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkMultiStateButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.LineDisplay; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkColor; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkColorLookup; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkMonoState; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class ClipLauncher { + + private final MpkColor[][] clipColors; + private final Transport transport; + private final SettableBooleanValue clipLauncherOverdub; + + public ClipLauncher(final MpkHwElements hwElements, final LayerCollection layerCollection, + final MpkViewControl viewControl, final Transport transport) { + final Layer mainLayer = layerCollection.get(LayerId.CLIP_LAUNCHER); + final List gridButtons = hwElements.getGridButtons(); + final TrackBank trackBank = viewControl.getTrackBank(); + final SceneBank sceneBank = trackBank.sceneBank(); + this.transport = transport; + clipColors = new MpkColor[trackBank.getSizeOfBank()][sceneBank.getSizeOfBank()]; + trackBank.setShouldShowClipLauncherFeedback(true); + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(trackIndex); + for (int j = 0; j < sceneBank.getSizeOfBank(); j++) { + final int sceneIndex = j; + clipColors[i][j] = MpkColor.OFF; + final ClipLauncherSlot clipSlot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + final int rowIndex = sceneBank.getSizeOfBank() - sceneIndex - 1; + final MpkMultiStateButton button = gridButtons.get(rowIndex * 4 + trackIndex); + button.bindLight(mainLayer, () -> getState(track, clipSlot, trackIndex, sceneIndex)); + button.bindIsPressed(mainLayer, pressed -> handleClipPress(pressed, clipSlot)); + prepareClipSlot(clipSlot, trackIndex, sceneIndex); + } + } + clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + final LineDisplay mainDisplay = hwElements.getMainLineDisplay(); + cursorTrack.name().addValueObserver(name -> mainDisplay.setText(0, 0, name)); + cursorTrack.color().addValueObserver((r, g, b) -> { + final int index = MpkColorLookup.rgbToIndex(r, g, b); + mainDisplay.setColorIndex(0, 0, index); + }); + } + + private void handleClipPress(final Boolean pressed, final ClipLauncherSlot clipSlot) { + if (pressed) { + clipSlot.launch(); + clipSlot.select(); + } else { + clipSlot.launchRelease(); + } + } + + private MpkColor getState(final Track track, final ClipLauncherSlot slot, final int trackIndex, + final int sceneIndex) { + if (slot.hasContent().get()) { + final MpkColor color = clipColors[trackIndex][sceneIndex]; + if (slot.isRecordingQueued().get()) { + return MpkColor.RED.variant(MpkMonoState.BLINK1_8); + } else if (slot.isRecording().get()) { + return MpkColor.RED.variant(MpkMonoState.PULSE1_4); + } else if (slot.isPlaybackQueued().get()) { + return color.variant(MpkMonoState.BLINK1_4); + } else if (slot.isStopQueued().get()) { + return MpkColor.GREEN.variant(MpkMonoState.BLINK1_8); + } else if (slot.isPlaying().get() && track.isQueuedForStop().get()) { + return MpkColor.GREEN.variant(MpkMonoState.BLINK1_8); + } else if (slot.isPlaying().get()) { + if (clipLauncherOverdub.get() && track.arm().get()) { + return MpkColor.RED.variant(MpkMonoState.PULSE1_4); + } else { + if (transport.isPlaying().get()) { + return MpkColor.GREEN.variant(MpkMonoState.PULSE1_4); + } + return MpkColor.GREEN; + } + } + return color; + } + if (slot.isRecordingQueued().get()) { + return MpkColor.RED.variant(MpkMonoState.PULSE1_8); + } else if (track.arm().get()) { + return MpkColor.RED.variant(MpkMonoState.SOLID_25); + } else if (slot.isPlaybackQueued().get()) { + return clipColors[trackIndex][sceneIndex].variant(MpkMonoState.BLINK1_16); + } + return MpkColor.OFF; + } + + private void prepareClipSlot(final ClipLauncherSlot slot, final int trackIndex, final int sceneIndex) { + slot.hasContent().markInterested(); + slot.isPlaying().markInterested(); + slot.isStopQueued().markInterested(); + slot.isRecordingQueued().markInterested(); + slot.isRecording().markInterested(); + slot.isPlaybackQueued().markInterested(); + slot.isSelected().markInterested(); + slot.color().addValueObserver((r, g, b) -> clipColors[trackIndex][sceneIndex] = MpkColor.getColor(r, g, b)); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/CursorTrackMixLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/CursorTrackMixLayer.java new file mode 100644 index 00000000..5a03ff01 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/CursorTrackMixLayer.java @@ -0,0 +1,103 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import java.util.List; + +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Send; +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.controllers.akai.mpkmk4.bindings.ParameterDisplayBinding; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.Encoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.LineDisplay; +import com.bitwig.extensions.framework.Layers; + +public class CursorTrackMixLayer extends EncoderLayer { + + private final SendBank sendBank; + private int sendsScrollPosition = 0; + private final LineDisplay display; + private boolean sendsChangedAction = false; + + public CursorTrackMixLayer(final Layers layers, final MpkHwElements hwElements, + final MpkMidiProcessor midiProcessor, final MpkViewControl viewControl) { + super("CURSOR_MIXER", layers, midiProcessor); + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + final List encoders = hwElements.getEncoders(); + display = hwElements.getMainLineDisplay(); + final Encoder panEncoder = encoders.get(0); + panEncoder.bindValue(this, cursorTrack.pan()); + this.addBinding(new ParameterDisplayBinding(cursorTrack.pan(), panEncoder, parameterValues, 0)); + final Encoder volumenEncoder = encoders.get(4); + volumenEncoder.bindValue(this, cursorTrack.volume()); + this.addBinding(new ParameterDisplayBinding(cursorTrack.volume(), volumenEncoder, parameterValues, 4)); + sendBank = cursorTrack.sendBank(); + sendBank.canScrollForwards().markInterested(); + sendBank.canScrollBackwards().markInterested(); + sendBank.scrollPosition().addValueObserver(this::handleScrollPosition); + sendBank.itemCount().addValueObserver(this::sendCountChanged); + for (int i = 0; i < 6; i++) { + final Send send = sendBank.getItemAt(i); + final int index = i + (i / 3 + 1); + final Encoder encoder = encoders.get(index); + encoder.bindValue(this, send); + this.addBinding(new ParameterDisplayBinding(send, encoder, parameterValues, index)); + } + } + + private void sendCountChanged(final int sends) { + if (isActive()) { + updateDisplay(sends); + } + } + + private void handleScrollPosition(final int pos) { + this.sendsScrollPosition = pos; + if (sendsChangedAction) { + display.temporaryInfo(1, "Sends", "%d-%d".formatted(sendsScrollPosition + 1, sendsScrollPosition + 6)); + sendsChangedAction = false; + } + updateDisplay(sendBank.itemCount().get()); + } + + @Override + public void navigateLeft() { + sendsChangedAction = true; + sendBank.scrollBackwards(); + } + + @Override + public void navigateRight() { + sendsChangedAction = true; + sendBank.scrollForwards(); + } + + @Override + public boolean canScrollRight() { + return sendBank.canScrollForwards().get(); + } + + @Override + public boolean canScrollLeft() { + return sendBank.canScrollBackwards().get(); + } + + @Override + protected void onActivate() { + updateDisplay(sendBank.itemCount().get()); + } + + private void updateDisplay(final int sendsCount) { + display.setText(1, "Track"); + if (sendsCount == 0) { + display.setText(2, "No Sends"); + } else if (sendsCount == 1) { + display.setText(2, "Send %d".formatted(sendsScrollPosition + 1)); + } else if (sendsCount > 1) { + display.setText( + 2, "Sends %d-%d".formatted(sendsScrollPosition + 1, Math.min(sendsScrollPosition + 6, sendsCount))); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/DrumPadLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/DrumPadLayer.java new file mode 100644 index 00000000..b3402115 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/DrumPadLayer.java @@ -0,0 +1,211 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import java.util.Arrays; +import java.util.List; + +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.InternalHardwareLightState; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extension.controller.api.PlayingNote; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.controllers.akai.mpkmk4.ScaleSetup; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkMultiStateButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkColor; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkMonoState; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.IntValueObject; + +public class DrumPadLayer extends Layer { + + private MpkColor trackColor; + private final ScaleSetup scaleSetup = new ScaleSetup(); + private final NoteInput noteInput; + private boolean hasDrumPads; + private final DrumPadBank focusPadBank; + protected final int[] noteToPad = new int[128]; + protected final int[] padToNote = new int[16]; + protected boolean[] isBaseNote = new boolean[16]; + protected final Integer[] deactivationTable = new Integer[128]; + private final Integer[] noteTable = new Integer[128]; + private final Integer[] velocityTable = new Integer[128]; + private final IntValueObject padOffset = new IntValueObject(36, 0, 120); + private final PadSlot[] padSlots = new PadSlot[16]; + + public DrumPadLayer(final Layers layers, final MpkHwElements hwElements, final LayerCollection layerCollection, + final MpkViewControl viewControl, final MpkMidiProcessor midiProcessor) { + super(layers, "DRUM_PADS"); + final List gridButtons = hwElements.getGridButtons(); + this.noteInput = midiProcessor.getNoteInput(); + Arrays.fill(deactivationTable, -1); + Arrays.fill(noteTable, -1); + Arrays.fill(noteToPad, -1); + Arrays.fill(padToNote, -1); + for (int i = 0; i < 128; i++) { + velocityTable[i] = i; + } + + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + cursorTrack.playingNotes().addValueObserver(this::handleNotePlaying); + + cursorTrack.color().addValueObserver((r, g, b) -> setTrackColor(MpkColor.getColor(r, g, b))); + final DrumPadBank drumPadBank = viewControl.getPadBank(); + viewControl.getPrimaryDevice().hasDrumPads().addValueObserver(this::handleHasDrumPadsChanged); + drumPadBank.setIndication(true); + drumPadBank.scrollPosition().addValueObserver(this::handlePadBankScrolling); + midiProcessor.addNoteListener(this::handlePlayingNote); + focusPadBank = viewControl.getFocusDrumPad(); + + for (int i = 0; i < 4; i++) { + final int colIndex = i; + for (int j = 0; j < 4; j++) { + final int rowIndex = j; + final int padIndex = rowIndex * 4 + colIndex; + final DrumPad pad = drumPadBank.getItemAt(padIndex); + final MpkMultiStateButton button = gridButtons.get(padIndex); + setUpPad(padIndex, pad); + button.bindLight(this, () -> getState(colIndex, rowIndex)); + } + } + scaleSetup.addChangeListener(this::handleScaleChanged); + padOffset.addValueObserver(v -> drumPadBank.scrollPosition().set(v)); + } + + private void handleScaleChanged() { + if (isActive()) { + applyScale(); + } + } + + public ScaleSetup getScaleSetup() { + return scaleSetup; + } + + public IntValueObject getPadOffset() { + return padOffset; + } + + private void handlePlayingNote(final int note, final int value) { + if (isActive()) { + final int padIndex = noteToPad[note]; + if (value > 0 && padIndex >= 0 && padIndex <= 16) { + if (hasDrumPads) { + final PadSlot padSlot = padSlots[padIndex]; + //padSlot.getPad().selectInMixer(); + padSlot.getDeviceItem().selectInEditor(); + //TODO Make this optional ?? + } + } + } + } + + public void init() { + Arrays.fill(noteTable, -1); + for (final PadSlot pad : padSlots) { + pad.setPlaying(false); + } + noteInput.setKeyTranslationTable(noteTable); + } + + private void setUpPad(final int index, final DrumPad pad) { + final PadSlot padSlot = new PadSlot(index, pad); + padSlots[index] = padSlot; + pad.name().markInterested(); + pad.exists().markInterested(); + pad.solo().markInterested(); + pad.mute().markInterested(); + pad.addIsSelectedInEditorObserver(selected -> padSlot.setSelected(selected)); + } + + private void handleHasDrumPadsChanged(final boolean hasDrumPads) { + this.hasDrumPads = hasDrumPads; + if (isActive()) { + applyScale(); + } + } + + private void handlePadBankScrolling(final int scrollPos) { + if (isActive()) { + applyScale(); + } + } + + private void setTrackColor(final MpkColor color) { + this.trackColor = color; + } + + private InternalHardwareLightState getState(final int colIndex, final int rowIndex) { + final int index = rowIndex * 4 + colIndex; + final PadSlot pad = padSlots[index]; + if (hasDrumPads) { + return pad.isPlaying() ? pad.getColor() : pad.getColor().variant(MpkMonoState.SOLID_10); + } + if (isBaseNote[index]) { + return pad.isPlaying() ? trackColor : trackColor.variant(MpkMonoState.SOLID_50); + } + return pad.isPlaying() ? trackColor : trackColor.variant(MpkMonoState.SOLID_10); + } + + + private void handleNotePlaying(final PlayingNote[] notes) { + if (isActive()) { + for (int i = 0; i < 16; i++) { + padSlots[i].setPlaying(false); + } + for (final PlayingNote playingNote : notes) { + final int padIndex = noteToPad[playingNote.pitch()]; + if (padIndex != -1) { + padSlots[padIndex].setPlaying(true); + } + } + } + } + + + private void applyScale() { + final int padOff = padOffset.get(); + Arrays.fill(noteToPad, -1); + Arrays.fill(padToNote, -1); + + if (hasDrumPads) { + for (int i = 0; i < 16; i++) { + noteTable[i + 0x24] = padOff + i; + noteToPad[padOff + i] = i; + padToNote[i] = padOff + i; + } + } else { + final List noteSequence = scaleSetup.getNoteSequence(16); + isBaseNote = getScaleSetup().getBaseNotes(16); + for (int i = 0; i < 16; i++) { + final int note = noteSequence.get(i); + noteTable[i + 0x24] = noteSequence.get(i); + noteToPad[note] = i; + padToNote[i] = note; + } + } + noteInput.setKeyTranslationTable(noteTable); + } + + @Override + protected void onActivate() { + super.onActivate(); + applyScale(); + } + + + @Override + protected void onDeactivate() { + super.onDeactivate(); + Arrays.fill(noteTable, -1); + for (int i = 0; i < 16; i++) { + padSlots[i].setPlaying(false); + } + noteInput.setKeyTranslationTable(noteTable); + this.noteInput.setShouldConsumeEvents(false); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/EncoderLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/EncoderLayer.java new file mode 100644 index 00000000..7069ddae --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/EncoderLayer.java @@ -0,0 +1,38 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import java.util.Optional; + +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.ParameterValues; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.RemotesDisplayControl; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public abstract class EncoderLayer extends Layer { + final protected MpkMidiProcessor midiProcessor; + protected final ParameterValues parameterValues; + + public EncoderLayer(final String name, final Layers layers, final MpkMidiProcessor midiProcessor) { + super(layers, name); + this.midiProcessor = midiProcessor; + this.parameterValues = new ParameterValues(midiProcessor); + } + + public Optional getDisplayControl() { + return Optional.empty(); + } + + public abstract void navigateLeft(); + + public abstract void navigateRight(); + + public abstract boolean canScrollRight(); + + public abstract boolean canScrollLeft(); + + @Override + protected void onActivate() { + super.onActivate(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/LayerCollection.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/LayerCollection.java new file mode 100644 index 00000000..8620cbc3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/LayerCollection.java @@ -0,0 +1,122 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extensions.controllers.akai.mpkmk4.GlobalStates; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkFocusClip; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkMultiStateButton; +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.ValueObject; + +@Component +public class LayerCollection { + + private final RemotesControlHandler remotesHandler; + + private final Map layerMap = new HashMap(); + private final LayerId[] initLayers = + {LayerId.NAVIGATION, LayerId.MAIN, LayerId.CLIP_LAUNCHER, LayerId.DEVICE_REMOTES}; + private final PinnableCursorDevice cursorDevice; + private final List gridButtons; + private int currentPadMode = 1; + private final DrumPadLayer drumPadLayer; + private final ValueObject padMode = new ValueObject<>(LayerId.CLIP_LAUNCHER); + + + public LayerCollection(final Layers layers, final MpkHwElements hwElements, final MpkMidiProcessor midiProcessor, + final MpkViewControl viewControl, final GlobalStates globalStates, final MpkFocusClip focusClip) { + + cursorDevice = viewControl.getCursorDevice(); + gridButtons = hwElements.getGridButtons(); + + remotesHandler = new RemotesControlHandler(layers, hwElements, midiProcessor, viewControl); + final NavigationLayer navigationLayer = + new NavigationLayer(layers, hwElements, globalStates, this, viewControl, focusClip); + + midiProcessor.registerMainDisplay(hwElements.getMainLineDisplay()); + midiProcessor.addModeChangeListener(this::handlePadModeChange); + midiProcessor.addUpdateListeners(this::handleUpdateNeeded); + drumPadLayer = new DrumPadLayer(layers, hwElements, this, viewControl, midiProcessor); + drumPadLayer.init(); + final PadMenuLayer padMenuLayer = new PadMenuLayer(layers, hwElements, this, viewControl, globalStates); + + for (final LayerId layerId : LayerId.values()) { + switch (layerId) { + case DEVICE_REMOTES -> layerMap.put(layerId, remotesHandler.getDeviceControlLayer()); + case TRACK_REMOTES -> layerMap.put(layerId, remotesHandler.getTrackControlLayer()); + case PROJECT_REMOTES -> layerMap.put(layerId, remotesHandler.getProjectControlLayer()); + case TRACK_CONTROL -> layerMap.put(layerId, remotesHandler.getCursorTrackMixerLayer()); + case MIX_CONTROL -> layerMap.put(layerId, remotesHandler.getMixerLayer()); + case NAVIGATION -> layerMap.put(layerId, navigationLayer); + case DRUM_PAD_CONTROL -> layerMap.put(layerId, drumPadLayer); + case PAD_MENU_LAYER -> layerMap.put(layerId, padMenuLayer); + default -> layerMap.put(layerId, new Layer(layers, layerId.toString())); + } + } + } + + private void handlePadModeChange(final int mode) { + if (mode == currentPadMode) { + return; + } + final Layer currentLayer = get(padModeToLayerId(currentPadMode)); + currentLayer.setIsActive(false); + final LayerId padModeLayerId = padModeToLayerId(mode); + padMode.set(padModeLayerId); + final Layer newLayer = get(padModeLayerId); + newLayer.setIsActive(true); + this.currentPadMode = mode; + } + + public ValueObject getPadMode() { + return padMode; + } + + public RemotesControlHandler getRemotesHandler() { + return remotesHandler; + } + + private LayerId padModeToLayerId(final int mode) { + return switch (mode) { + case 2 -> LayerId.TRACK_PAD_CONTROL; + case 0 -> LayerId.DRUM_PAD_CONTROL; + default -> LayerId.CLIP_LAUNCHER; + }; + } + + private void handleUpdateNeeded() { + for (final MpkMultiStateButton button : gridButtons) { + button.forceUpdate(); + } + } + + public Layer get(final LayerId layerId) { + final Layer layer = layerMap.get(layerId); + if (layer == null) { + throw new RuntimeException("Layer %s does not exists".formatted(layerId)); + } + return layer; + } + + @Activate + public void init() { + for (final LayerId id : initLayers) { + layerMap.get(id).setIsActive(true); + } + } + + public DrumPadLayer getDrumPadLayer() { + return drumPadLayer; + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/LayerId.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/LayerId.java new file mode 100644 index 00000000..60d54503 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/LayerId.java @@ -0,0 +1,31 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +public enum LayerId { + MAIN, + CLIP_LAUNCHER, + TRACK_PAD_CONTROL, + DRUM_PAD_CONTROL, + DEVICE_REMOTES(true), + PROJECT_REMOTES(true), + TRACK_REMOTES(true), + TRACK_CONTROL, + MIX_CONTROL, + NAVIGATION, + PAD_MENU_LAYER, + SHIFT, + OVER_LAYER; + + private final boolean controlsDevice; + + LayerId(final boolean controlsDevice) { + this.controlsDevice = controlsDevice; + } + + LayerId() { + this(false); + } + + public boolean isControlsDevice() { + return controlsDevice; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/MixEncoderLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/MixEncoderLayer.java new file mode 100644 index 00000000..658de2a3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/MixEncoderLayer.java @@ -0,0 +1,149 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import java.util.List; + +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.controllers.akai.mpkmk4.bindings.ParameterDisplayBinding; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.Encoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.LineDisplay; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.ParameterValues; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public class MixEncoderLayer extends EncoderLayer { + + private final TrackBank trackBank; + private final Layer sendsLayer; + private final ParameterValues sendParameterValues; + private int sendScrollPos; + private final SendBank sendBank; + private final LineDisplay display; + + public MixEncoderLayer(final Layers layers, final MpkHwElements hwElements, final MpkMidiProcessor midiProcessor, + final MpkViewControl viewControl) { + super("GRID_MIXER", layers, midiProcessor); + trackBank = viewControl.getTrackBank(); + final List encoders = hwElements.getEncoders(); + sendsLayer = new Layer(layers, "GRID_SENDS"); + this.sendParameterValues = new ParameterValues(midiProcessor); + sendBank = trackBank.getItemAt(0).sendBank(); + sendBank.scrollPosition().addValueObserver(this::handleScrollPos); + display = hwElements.getMainLineDisplay(); + + for (int i = 0; i < 4; i++) { + final Track track = trackBank.getItemAt(i); + final Encoder encoderPan = encoders.get(i); + encoderPan.bindValue(this, track.pan()); + this.addBinding(new ParameterDisplayBinding(track.pan(), encoderPan, parameterValues, i)); + final Encoder encoderVolume = encoders.get(i + 4); + encoderVolume.bindValue(this, track.volume()); + this.addBinding(new ParameterDisplayBinding(track.volume(), encoderVolume, parameterValues, i + 4)); + + final SendBank sendsBank = track.sendBank(); + sendsBank.canScrollForwards().markInterested(); + sendsBank.canScrollBackwards().markInterested(); + + encoderPan.bindValue(sendsLayer, sendsBank.getItemAt(0)); + sendsLayer.addBinding( + new ParameterDisplayBinding(sendsBank.getItemAt(0), encoderPan, sendParameterValues, i)); + + encoderVolume.bindValue(sendsLayer, sendsBank.getItemAt(1)); + sendsLayer.addBinding( + new ParameterDisplayBinding(sendsBank.getItemAt(1), encoderVolume, sendParameterValues, i + 4)); + } + } + + private void handleScrollPos(final int scrollPos) { + this.sendScrollPos = scrollPos; + if (isSendsActive()) { + display.temporaryInfo(1, "Mixer", getSendScrollInfo()); + } + } + + public String getSendScrollInfo() { + final String name1 = sendBank.getItemAt(0).name().get(); + final String name2 = sendBank.getItemAt(1).name().get(); + return "%s/%s".formatted(reduce(name1, 4), reduce(name2, 4)); + } + + private static String reduce(final String name, final int len) { + return name.substring(0, Math.min(name.length(), len)); + } + + public String getSendInfo() { + return "Sends %d-%d".formatted(sendScrollPos + 1, sendScrollPos + 2); + } + + public void toggleSends() { + sendsLayer.toggleIsActive(); + updateDisplay(); + if (sendsLayer.isActive()) { + display.temporaryInfo(1, "Mixer Sends", getSendInfo()); + } else { + display.temporaryInfo(1, "Mixer", "Pan/Volume"); + } + } + + public boolean isSendsActive() { + return sendsLayer.isActive(); + } + + @Override + public void navigateLeft() { + if (sendsLayer.isActive()) { + if (sendScrollPos == 0) { + toggleSends(); + } else { + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + trackBank.getItemAt(i).sendBank().scrollBackwards(); + } + } + } + } + + @Override + public void navigateRight() { + if (sendsLayer.isActive()) { + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + trackBank.getItemAt(i).sendBank().scrollForwards(); + } + } else { + toggleSends(); + } + } + + @Override + public boolean canScrollRight() { + if (sendsLayer.isActive()) { + return sendBank.canScrollForwards().get(); + } + return true; + } + + @Override + public boolean canScrollLeft() { + return sendsLayer.isActive(); + } + + @Override + protected void onActivate() { + updateDisplay(); + } + + private void updateDisplay() { + display.setText(1, "Mixer"); + display.setText( + 2, sendsLayer.isActive() ? "Sends %d/%d".formatted(sendScrollPos + 1, sendScrollPos + 2) : "Volume/Pan"); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + sendsLayer.setIsActive(false); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/NavigationLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/NavigationLayer.java new file mode 100644 index 00000000..b27f600b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/NavigationLayer.java @@ -0,0 +1,133 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Scene; +import com.bitwig.extension.controller.api.SceneBank; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.akai.apc.common.control.ClickEncoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.GlobalStates; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkFocusClip; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkCcAssignment; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkMultiStateButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.LineDisplay; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkColorLookup; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public class NavigationLayer extends Layer { + + private final Layer shiftLayer; + private final LayerCollection layerCollection; + private final SceneBank scenesBank; + private final TrackBank trackBank; + private final Scene focusScene; + private final LineDisplay display; + private final LineDisplay menuDisplay; + private int sceneColor = 0; + + private final MpkFocusClip focusClip; + private boolean encoderSceneAction; + private final RemotesControlHandler remotesControlHandler; + + public NavigationLayer(final Layers layers, final MpkHwElements hwElements, final GlobalStates globalStates, + final LayerCollection layerCollection, final MpkViewControl viewControl, final MpkFocusClip focusClip) { + super(layers, "NAVIGATION"); + this.layerCollection = layerCollection; + remotesControlHandler = layerCollection.getRemotesHandler(); + this.trackBank = viewControl.getTrackBank(); + this.focusClip = focusClip; + this.trackBank.sceneBank().scrollPosition().markInterested(); + shiftLayer = new Layer(layers, "NAVIGATION_SHIFT_LAYER"); + globalStates.getShiftHeld().addValueObserver(shift -> { + if (isActive()) { + shiftLayer.setIsActive(shift); + } + }); + display = hwElements.getMainLineDisplay(); + menuDisplay = hwElements.getMenuLineDisplay(); + scenesBank = viewControl.getFocusTrackBank().sceneBank(); + scenesBank.setIndication(true); + scenesBank.scrollPosition().addValueObserver(this::handleSceneScrollPosition); + focusScene = scenesBank.getScene(0); + focusScene.color().addValueObserver((r, g, b) -> this.updateSceneColor(MpkColorLookup.rgbToIndex(r, g, b))); + focusScene.name().addValueObserver(this::handleSceneNameChanged); + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + final MpkMultiStateButton leftButton = hwElements.getButton(MpkCcAssignment.BANK_LEFT); + final MpkMultiStateButton rightButton = hwElements.getButton(MpkCcAssignment.BANK_RIGHT); + leftButton.bindLightOnOff(this, cursorTrack.hasPrevious()); + leftButton.bindRepeatHold(this, () -> cursorTrack.selectPrevious()); + rightButton.bindLightOnOff(this, cursorTrack.hasNext()); + rightButton.bindRepeatHold(this, () -> cursorTrack.selectNext()); + + leftButton.bindLightOnOff(shiftLayer, () -> remotesControlHandler.canNavigateLeft()); + leftButton.bindRepeatHold(shiftLayer, () -> remotesControlHandler.navigateLeft()); + rightButton.bindLightOnOff(shiftLayer, () -> remotesControlHandler.canNavigateRight()); + rightButton.bindRepeatHold(shiftLayer, () -> remotesControlHandler.navigateRight()); + + final ClickEncoder encoder = hwElements.getMainEncoder(); + final MpkButton encoderButton = hwElements.getMainEncoderPressButton(); + encoder.bind(this, this::sceneSelection); + encoderButton.bindIsPressed(this, this::handleEncoderPressed); + encoder.bind(shiftLayer, remotesControlHandler::handleShiftEncoderTurn); + encoderButton.bindIsPressed(shiftLayer, this::changeMode); + } + + private void updateSceneColor(final int colorIndex) { + sceneColor = colorIndex == 102 ? 0 : colorIndex; + display.setColorIndex(1, 1, sceneColor); + } + + private void handleSceneNameChanged(final String sceneName) { + if (encoderSceneAction) { + display.setText(1, 1, focusScene.name().get()); + encoderSceneAction = false; + } + } + + private void handleSceneScrollPosition(final int pos) { + final int trackBankPosition = trackBank.sceneBank().scrollPosition().get(); + if (pos < trackBankPosition) { + trackBank.sceneBank().scrollPosition().set(pos); + } else if (pos >= (trackBankPosition + 4)) { + trackBank.sceneBank().scrollPosition().set(pos - 3); + } + focusClip.setSelectedSlotIndex(pos); + } + + private void changeMode(final Boolean pressed) { + if (pressed) { + if (layerCollection.getPadMode().get() == LayerId.DRUM_PAD_CONTROL) { + final Layer menuLayer = layerCollection.get(LayerId.PAD_MENU_LAYER); + menuLayer.setIsActive(true); + } else { + remotesControlHandler.incrementEncoderMode(1, true); + display.temporaryInfo(1, "Knob Mode", remotesControlHandler.getEncoderModeValue().get()); + } + } + } + + private void handleEncoderPressed(final Boolean pressed) { + if (!pressed) { + return; + } + scenesBank.getScene(0).launch(); + } + + private void sceneSelection(final int inc) { + scenesBank.scrollBy(inc); + display.activateTemporary(1); + display.setText(1, 1, focusScene.name().get()); + display.setColorIndex(1, 1, sceneColor); + display.setText(1, 2, ""); + encoderSceneAction = true; + } + + @Override + protected void onDeactivate() { + this.shiftLayer.setIsActive(false); + layerCollection.get(LayerId.PAD_MENU_LAYER).setIsActive(false); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/PadMenuLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/PadMenuLayer.java new file mode 100644 index 00000000..54f5166c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/PadMenuLayer.java @@ -0,0 +1,181 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extensions.controllers.akai.apc.common.control.ClickEncoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.GlobalStates; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.controllers.akai.mpkmk4.ScaleSetup; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkCcAssignment; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkMultiStateButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.LineDisplay; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MenuEntry; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MenuList; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkDisplayFont; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.IntValueObject; +import com.bitwig.extensions.framework.values.Scale; +import com.bitwig.extensions.framework.values.ValueObject; + +public class PadMenuLayer extends Layer { + + private final LineDisplay display; + private final LineDisplay menuDisplay; + + private final Layer internalShiftLayer; + + private final MenuList noteMenuList = new MenuList(); + private final MenuList padMenuList = new MenuList(); + private MenuList currentMenuList = padMenuList; + + private final GlobalStates states; + private MenuEntry currentMenu; + private final DrumPadLayer padLayer; + private boolean hasDrumPads; + + + public PadMenuLayer(final Layers layers, final MpkHwElements hwElements, final LayerCollection layerCollection, + final MpkViewControl viewControl, final GlobalStates states) { + super(layers, "PAD_MENU_LAYER"); + display = hwElements.getMainLineDisplay(); + menuDisplay = hwElements.getMenuLineDisplay(); + padLayer = layerCollection.getDrumPadLayer(); + internalShiftLayer = new Layer(layers, "MENU_SHIFT"); + this.states = states; + final ScaleSetup scaleSetup = padLayer.getScaleSetup(); + final IntValueObject baseNote = scaleSetup.getBaseNote(); + final BasicStringValue baseNoteString = new BasicStringValue(ScaleSetup.toNote(baseNote.get())); + baseNote.addValueObserver(v -> baseNoteString.set(ScaleSetup.toNote(v))); + + viewControl.getPrimaryDevice().hasDrumPads().addValueObserver(this::handleHasDrumPadsChanged); + + final ValueObject scaleValue = scaleSetup.getScale(); + final BasicStringValue scaleString = new BasicStringValue(scaleValue.get().getShortName()); + scaleValue.addValueObserver(scale -> scaleString.set(scale.getShortName())); + + final IntValueObject octaveValue = scaleSetup.getOctaveOffset(); + final BasicStringValue octaveString = new BasicStringValue(scaleSetup.getStartInfo()); + octaveValue.addValueObserver(v -> octaveString.set(scaleSetup.getStartInfo())); + + final IntValueObject padOffsetValue = padLayer.getPadOffset(); + final BasicStringValue padOffsetString = new BasicStringValue(Integer.toString(padOffsetValue.get())); + padOffsetValue.addValueObserver(v -> padOffsetString.set(Integer.toString(v))); + + final RemotesControlHandler remotesHandler = layerCollection.getRemotesHandler(); + + noteMenuList.add("Exit", () -> setIsActive(false)); + noteMenuList.add("Scale", scaleString, scaleValue::increment); + noteMenuList.add("Root", baseNoteString, baseNote::increment); + noteMenuList.add("Oct", octaveString, octaveValue::increment); + noteMenuList.add( + "Enc.M", remotesHandler.getEncoderModeValue(), + inc -> remotesHandler.incrementEncoderMode(inc, false)); + + padMenuList.add("Exit", () -> setIsActive(false)); + padMenuList.add("Pad Off", padOffsetString, inc -> padOffsetValue.increment(inc * 4)); + padMenuList.add( + "Enc.M", remotesHandler.getEncoderModeValue(), + inc -> remotesHandler.incrementEncoderMode(inc, false)); + + currentMenu = currentMenuList.get(0); + final ClickEncoder encoder = hwElements.getMainEncoder(); + final MpkButton encoderButton = hwElements.getMainEncoderPressButton(); + encoder.bind(this, this::incrementEncoder); + encoderButton.bindIsPressed(this, this::handleEncoderButton); + final MpkMultiStateButton leftButton = hwElements.getButton(MpkCcAssignment.BANK_LEFT); + final MpkMultiStateButton rightButton = hwElements.getButton(MpkCcAssignment.BANK_RIGHT); + leftButton.bindLightPressedOnDimmed(this); + leftButton.bindRepeatHold(this, () -> incrementValue(-1)); + rightButton.bindLightPressedOnDimmed(this); + rightButton.bindRepeatHold(this, () -> incrementValue(1)); + + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + leftButton.bindLightOnOff(internalShiftLayer, cursorTrack.hasPrevious()); + leftButton.bindRepeatHold(internalShiftLayer, () -> cursorTrack.selectPrevious()); + rightButton.bindLightOnOff(internalShiftLayer, cursorTrack.hasNext()); + rightButton.bindRepeatHold(internalShiftLayer, () -> cursorTrack.selectNext()); + + states.getShiftHeld().addValueObserver(shift -> { + if (isActive()) { + internalShiftLayer.setIsActive(shift); + } + }); + layerCollection.getPadMode().addValueObserver(this::handlePadModChange); + } + + private void handleHasDrumPadsChanged(final boolean hasDrumPads) { + this.hasDrumPads = hasDrumPads; + if (isActive()) { + updateCurrentMenuList(); + } + } + + private void updateCurrentMenuList() { + currentMenuList.resetValueFocus(); + currentMenuList = hasDrumPads ? padMenuList : noteMenuList; + currentMenuList.updateDisplay(menuDisplay); + } + + private void handlePadModChange(final LayerId layerId) { + if (isActive() && layerId != LayerId.DRUM_PAD_CONTROL) { + setIsActive(false); + } + } + + private void incrementEncoder(final int inc) { + if (currentMenuList.onValue()) { + incrementValue(inc); + } else { + if (currentMenuList.increment(inc)) { + currentMenu = currentMenuList.getCurrent(); + currentMenuList.updateDisplay(menuDisplay); + } + } + } + + private void incrementValue(final int inc) { + if (currentMenu.getIncrementHandler() != null) { + currentMenu.getIncrementHandler().accept(inc); + currentMenuList.updateMenuEntry(currentMenu, menuDisplay); // Maybe update only on change + } + } + + private void handleEncoderButton(final boolean pressed) { + if (!pressed) { + return; + } + if (states.getShiftHeld().get()) { + this.setIsActive(false); + } else { + if (currentMenu.getClickHandler() != null) { + currentMenu.getClickHandler().run(); + } else { + currentMenuList.toggleValueFocus(); + currentMenuList.updateDisplay(menuDisplay); + } + } + } + + + @Override + protected void onActivate() { + super.onActivate(); + display.setActive(false); + menuDisplay.setActive(true); + for (int i = 0; i < 3; i++) { + menuDisplay.setMenuLine(i, MpkDisplayFont.PT24_BOLD, 0, MenuList.FOREGROUND, MenuList.BACKGROUND); + } + updateCurrentMenuList(); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + menuDisplay.setActive(false); + display.setActive(true); + internalShiftLayer.setIsActive(false); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/PadSlot.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/PadSlot.java new file mode 100644 index 00000000..e7a8b80e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/PadSlot.java @@ -0,0 +1,73 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.DrumPad; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkColor; + +public class PadSlot { + private final DrumPad pad; + private final int index; + private final Device deviceItem; + private MpkColor color = MpkColor.OFF; + private boolean isSelected; + private boolean isBaseNote; + private boolean playing; + + public PadSlot(final int index, final DrumPad pad) { + this.pad = pad; + this.index = index; + pad.color().addValueObserver((r, g, b) -> color = MpkColor.getColor(r, g, b)); + pad.name().markInterested(); + final DeviceBank deviceBank = pad.createDeviceBank(1); + deviceItem = deviceBank.getDevice(0); + } + + public MpkColor getColor() { + return color; + } + + public Device getDeviceItem() { + return deviceItem; + } + + public DrumPad getPad() { + return pad; + } + + public String getName() { + return pad.name().get(); + } + + public boolean isPlaying() { + return playing; + } + + public void setColor(final MpkColor color) { + this.color = color; + } + + public boolean isSelected() { + return isSelected; + } + + public void setSelected(final boolean selected) { + isSelected = selected; + } + + public boolean isBaseNote() { + return isBaseNote; + } + + public void setBaseNote(final boolean baseNote) { + isBaseNote = baseNote; + } + + public void setPlaying(final boolean playing) { + this.playing = playing; + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/RemotesControlHandler.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/RemotesControlHandler.java new file mode 100644 index 00000000..09fbdd14 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/RemotesControlHandler.java @@ -0,0 +1,189 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.BasicStringValue; + +public class RemotesControlHandler { + public static final int BASIC_ENCODER_MODE_NO = 3; + + private final PinnableCursorDevice cursorDevice; + private EncoderLayer remotesControlLayer; + private final RemotesControlLayer deviceControlLayer; + private final RemotesControlLayer trackControlLayer; + private final RemotesControlLayer projectControlLayer; + private LayerId focusDeviceControl = LayerId.DEVICE_REMOTES; + private LayerId encoderLayerMode = LayerId.DEVICE_REMOTES; + private final BasicStringValue encoderModeValue = new BasicStringValue(); + private final MixEncoderLayer mixerLayer; + private final CursorTrackMixLayer cursorTrackMixerLayer; + private int selectedEncoderMode = 0; + + public RemotesControlHandler(final Layers layers, final MpkHwElements hwElements, + final MpkMidiProcessor midiProcessor, final MpkViewControl viewControl) { + cursorDevice = viewControl.getCursorDevice(); + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + encoderModeValue.set(encoderModeToString()); + + deviceControlLayer = new RemotesControlLayer( + "DEVICE", layers, cursorDevice.name(), cursorDevice.createCursorRemoteControlsPage(8), hwElements, + midiProcessor); + trackControlLayer = new RemotesControlLayer( + "TRACK", layers, new BasicStringValue("Track Remotes"), cursorTrack.createCursorRemoteControlsPage(8), + hwElements, midiProcessor); + projectControlLayer = new RemotesControlLayer( + "PROJECT", layers, new BasicStringValue("Project Remotes"), + viewControl.getRootTrack().createCursorRemoteControlsPage(8), hwElements, midiProcessor); + cursorTrackMixerLayer = new CursorTrackMixLayer(layers, hwElements, midiProcessor, viewControl); + mixerLayer = new MixEncoderLayer(layers, hwElements, midiProcessor, viewControl); + remotesControlLayer = deviceControlLayer; + remotesControlLayer.getDisplayControl() // + .ifPresent(control -> control.setActive(true)); + } + + private String encoderModeToString() { + return switch (encoderLayerMode) { + case TRACK_REMOTES, PROJECT_REMOTES, DEVICE_REMOTES -> "Remotes"; + case MIX_CONTROL -> "Mixer"; + case TRACK_CONTROL -> "Track"; + default -> ""; + }; + } + + public CursorTrackMixLayer getCursorTrackMixerLayer() { + return cursorTrackMixerLayer; + } + + public MixEncoderLayer getMixerLayer() { + return mixerLayer; + } + + public RemotesControlLayer getDeviceControlLayer() { + return deviceControlLayer; + } + + public RemotesControlLayer getTrackControlLayer() { + return trackControlLayer; + } + + public RemotesControlLayer getProjectControlLayer() { + return projectControlLayer; + } + + public void handleShiftEncoderTurn(final int inc) { + if (encoderLayerMode == LayerId.MIX_CONTROL || encoderLayerMode == LayerId.TRACK_CONTROL) { + if (inc > 0) { + this.cursorDevice.selectNext(); + } else { + this.cursorDevice.selectPrevious(); + } + getFocussedDeviceControl().updateTemporary(); + } else { + selectDevice(inc); + } + } + + public void selectDevice(final int inc) { + if (encoderLayerMode == LayerId.DEVICE_REMOTES) { + if (inc > 0) { + this.cursorDevice.selectNext(); + } else { + if (this.cursorDevice.hasPrevious().get()) { + this.cursorDevice.selectPrevious(); + } else { + setEncoderLayerMode(LayerId.TRACK_REMOTES); + } + } + } else if (encoderLayerMode == LayerId.TRACK_REMOTES) { + if (inc > 0) { + setEncoderLayerMode(LayerId.DEVICE_REMOTES); + } else { + setEncoderLayerMode(LayerId.PROJECT_REMOTES); + } + } else if (encoderLayerMode == LayerId.PROJECT_REMOTES) { + if (inc > 0) { + setEncoderLayerMode(LayerId.TRACK_REMOTES); + } + } + } + + public void incrementEncoderMode(final int inc, final boolean roundRobin) { + final int previousMode = selectedEncoderMode; + int nextMode = selectedEncoderMode + inc; + if (roundRobin) { + nextMode = nextMode % BASIC_ENCODER_MODE_NO; + } else { + nextMode = Math.max(0, Math.min(BASIC_ENCODER_MODE_NO - 1, nextMode)); + } + if (nextMode != previousMode) { + selectedEncoderMode = nextMode; + if (selectedEncoderMode == 0) { + backToDeviceControl(); + } else if (selectedEncoderMode == 1) { + setEncoderLayerMode(LayerId.TRACK_CONTROL); + } else if (selectedEncoderMode == 2) { + setEncoderLayerMode(LayerId.MIX_CONTROL); + } + } + } + + public void backToDeviceControl() { + setEncoderLayerMode(focusDeviceControl); + } + + public BasicStringValue getEncoderModeValue() { + return encoderModeValue; + } + + private EncoderLayer get(final LayerId layerId) { + return switch (layerId) { + case PROJECT_REMOTES -> projectControlLayer; + case TRACK_REMOTES -> trackControlLayer; + case TRACK_CONTROL -> cursorTrackMixerLayer; + case MIX_CONTROL -> mixerLayer; + default -> deviceControlLayer; + }; + } + + private RemotesControlLayer getFocussedDeviceControl() { + return switch (focusDeviceControl) { + case PROJECT_REMOTES -> projectControlLayer; + case TRACK_REMOTES -> trackControlLayer; + default -> deviceControlLayer; + }; + } + + public boolean canNavigateLeft() { + return this.remotesControlLayer.canScrollLeft(); + } + + public boolean canNavigateRight() { + return this.remotesControlLayer.canScrollRight(); + } + + public void navigateLeft() { + this.remotesControlLayer.navigateLeft(); + } + + public void navigateRight() { + this.remotesControlLayer.navigateRight(); + } + + public void setEncoderLayerMode(final LayerId id) { + if (id == encoderLayerMode) { + return; + } + this.encoderLayerMode = id; + encoderModeValue.set(encoderModeToString()); + this.remotesControlLayer.setIsActive(false); + this.remotesControlLayer = get(id); + this.remotesControlLayer.setIsActive(true); + if (this.encoderLayerMode.isControlsDevice()) { + focusDeviceControl = this.encoderLayerMode; + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/RemotesControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/RemotesControlLayer.java new file mode 100644 index 00000000..082e96cd --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/RemotesControlLayer.java @@ -0,0 +1,77 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.RemoteControl; +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.bindings.ParameterDisplayBinding; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.Encoder; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.RemotesDisplayControl; +import com.bitwig.extensions.framework.Layers; + +public class RemotesControlLayer extends EncoderLayer { + + private final CursorRemoteControlsPage deviceRemotes; + private final RemotesDisplayControl displayControl; + private final List bindings = new ArrayList<>(); + + public RemotesControlLayer(final String name, final Layers layers, final StringValue deviceNameValue, + final CursorRemoteControlsPage deviceRemotes, final MpkHwElements hwElements, + final MpkMidiProcessor midiProcessor) { + super(name, layers, midiProcessor); + this.deviceRemotes = deviceRemotes; + this.displayControl = + new RemotesDisplayControl(deviceNameValue, hwElements.getMainLineDisplay(), deviceRemotes); + final List encoders = hwElements.getEncoders(); + for (int i = 0; i < 8; i++) { + final RemoteControl remote = deviceRemotes.getParameter(i); + final Encoder encoder = encoders.get(i); + encoder.bindValue(this, remote.value()); + final ParameterDisplayBinding binding = new ParameterDisplayBinding(remote, encoder, parameterValues, i); + this.bindings.add(binding); + this.addBinding(binding); + } + } + + public void updateTemporary() { + displayControl.updateTemporary(); + } + + public Optional getDisplayControl() { + return Optional.of(displayControl); + } + + public void navigateLeft() { + deviceRemotes.selectPreviousPage(false); + } + + public void navigateRight() { + deviceRemotes.selectNextPage(false); + } + + public boolean canScrollRight() { + return this.displayControl.canScrollRight(); + } + + public boolean canScrollLeft() { + return this.displayControl.canScrollLeft(); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + this.displayControl.setActive(false); + } + + @Override + protected void onActivate() { + super.onActivate(); + displayControl.setActive(true); + this.displayControl.updateDisplay(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/TrackPadControl.java b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/TrackPadControl.java new file mode 100644 index 00000000..0ea90b52 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/layers/TrackPadControl.java @@ -0,0 +1,105 @@ +package com.bitwig.extensions.controllers.akai.mpkmk4.layers; + +import java.util.List; + +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.Project; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkHwElements; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkMidiProcessor; +import com.bitwig.extensions.controllers.akai.mpkmk4.MpkViewControl; +import com.bitwig.extensions.controllers.akai.mpkmk4.controls.MpkMultiStateButton; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkColor; +import com.bitwig.extensions.controllers.akai.mpkmk4.display.MpkMonoState; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class TrackPadControl { + + private int armHeld; + private int soloHeld; + private final Project project; + + public TrackPadControl(final MpkHwElements hwElements, final LayerCollection layerCollection, + final MpkViewControl viewControl, final Project project, final MpkMidiProcessor midiProcessor) { + final Layer mainLayer = layerCollection.get(LayerId.TRACK_PAD_CONTROL); + final List gridButtons = hwElements.getGridButtons(); + final TrackBank trackBank = viewControl.getTrackBank(); + this.project = project; + trackBank.setShouldShowClipLauncherFeedback(true); + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(trackIndex); + final MpkMultiStateButton armButton = gridButtons.get(trackIndex); + armButton.bindLight(mainLayer, () -> getArmState(track)); + armButton.bindIsPressed(mainLayer, pressed -> handleArmPressed(pressed, track)); + final MpkMultiStateButton stopButton = gridButtons.get(trackIndex + 4); + stopButton.bindLight(mainLayer, () -> getStopState(track)); + stopButton.bindPressed(mainLayer, () -> track.stop()); + final MpkMultiStateButton muteButton = gridButtons.get(8 + trackIndex); + muteButton.bindLight(mainLayer, () -> getMuteState(track)); + muteButton.bindPressed(mainLayer, () -> track.mute().toggle()); + final MpkMultiStateButton soloButton = gridButtons.get(12 + trackIndex); + soloButton.bindLight(mainLayer, () -> getSoloState(track)); + soloButton.bindIsPressed(mainLayer, pressed -> handleSoloPressed(pressed, track)); + } + } + + private InternalHardwareLightState getMuteState(final Track track) { + return track.mute().get() ? MpkColor.ORANGE : MpkColor.ORANGE.variant(MpkMonoState.SOLID_10); + } + + private InternalHardwareLightState getStopState(final Track track) { + if (track.exists().get()) { + if (track.isQueuedForStop().get()) { + return MpkColor.GREEN.variant(MpkMonoState.BLINK1_4); + } else if (track.isStopped().get()) { + return MpkColor.GREEN.variant(MpkMonoState.SOLID_10); + } + return MpkColor.GREEN; + } + return MpkColor.OFF; + } + + private InternalHardwareLightState getSoloState(final Track track) { + return track.solo().get() ? MpkColor.YELLOW : MpkColor.YELLOW.variant(MpkMonoState.SOLID_10); + } + + private void handleArmPressed(final boolean pressed, final Track track) { + if (pressed) { + armHeld++; + if (armHeld == 1) { + if (track.arm().get()) { + track.arm().set(false); + } else { + project.unarmAll(); + track.arm().set(true); + } + } + } else { + armHeld = Math.max(0, armHeld - 1); + } + + } + + private void handleSoloPressed(final boolean pressed, final Track track) { + if (pressed) { + soloHeld++; + if (soloHeld == 1) { + track.solo().toggle(true); + } else if (soloHeld > 1) { + track.solo().toggle(false); + } + } else { + soloHeld = Math.max(0, soloHeld - 1); + } + + } + + private InternalHardwareLightState getArmState(final Track track) { + return track.arm().get() ? MpkColor.RED : MpkColor.RED.variant(MpkMonoState.SOLID_10); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/todo.adoc b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/todo.adoc new file mode 100644 index 00000000..8b2c58fd --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/mpkmk4/todo.adoc @@ -0,0 +1,14 @@ +== Akai MPK Mini IV Todos + +- [x] Quantize = Record Quantize +- [x] Parameter Artefakte Bug +- [x] Mixer / Track Modes show the mode statically +- [x] Pad Offset in Menu only when Drum Device / etc. +- [x] Quantize change with Encoder while holding +- [x] In Menu SHIFT +/- Still need to move tracks +- [x] Changing Device in Mixer Mode -> Temporary Info +- [x] Mixer shows Sends +- [x] Fix Focus Rec/Launch Function +- [ ] Improved Clip Colors + +- [ ] Introduce a timed variable diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/DeviceHwElements.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/DeviceHwElements.java new file mode 100644 index 00000000..bd3d2dcc --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/DeviceHwElements.java @@ -0,0 +1,188 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.DoubleConsumer; + +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.AbsoluteHardwareKnob; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneAssignButton; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneEncoder; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneRgbButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.values.MidiStatus; + +public class DeviceHwElements { + private static final DoubleConsumer EMPTY_CONSUMER = v -> {}; + + private final XoneEncoder layerEncoder; + private final XoneEncoder shiftEncoder; + private final XoneRgbButton shiftButton; + private final XoneRgbButton layerButton; + public static final int BASE_CHANNEL = 0xE; + private final List gridButtons = new ArrayList<>(); + private final List knobButtons = new ArrayList<>(); + private final List sliders = new ArrayList<>(); + private final List knobs = new ArrayList<>(); + private final List encoders = new ArrayList<>(); + + private static final int[] BASE_TOP_ENCODER_CC = {0x16, 0x2C}; + private static final int[] BASE_BOTTOM_ENCODER_CC = {0x2A, 0x44}; + private static final int[] LAYER_BUTTON_BASE = {0x3C, 0x60}; // TOP is + 0x16 + private static final int[] ENCODER_BUTTON_BASE = {0x58, 0x7C}; // TOP is + 0x16 + + private static final int[] ENCODER_BUTTONS_INDEX = { + 0x30, 0x31, 0x32, 0x33, // + 0x2C, 0x2D, 0x2E, 0x2F, // + 0x28, 0x29, 0x2A, 0x2B, // + }; + + private static final int[] GRID_BUTTONS_INDEX = { + 0x24, 0x25, 0x26, 0x27, // + 0x20, 0x21, 0x22, 0x23, // + 0x1C, 0x1D, 0x1E, 0x1F, // + 0x18, 0x19, 0x1A, 0x1B, + }; + + + public DeviceHwElements(final int deviceIndex, final HardwareSurface surface, final XoneMidiDevice midiProcessor, + final XoneK3GlobalStates globalStates) { + for (int i = 0; i < 16; i++) { + final XoneRgbButton button = new XoneRgbButton( + i, 0xC + i, "GRID%d".formatted(deviceIndex), MidiStatus.NOTE_ON, GRID_BUTTONS_INDEX[i], BASE_CHANNEL, + surface, midiProcessor); + gridButtons.add(button); + } + final MidiIn midiIn = midiProcessor.getMidiIn(); + for (int i = 0; i < 4; i++) { + final AbsoluteHardwareKnob control = + surface.createAbsoluteHardwareKnob("SLIDER%d-%d".formatted(deviceIndex, i + 1)); + control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(BASE_CHANNEL, 0x10 + i)); + sliders.add(control); + encoders.add( + new XoneEncoder( + i, 0x1E + i, BASE_CHANNEL, i, 0x34, "ENCODER%d".formatted(deviceIndex), midiProcessor, surface)); + } + for (int i = 0; i < 12; i++) { + final AbsoluteHardwareKnob control = + surface.createAbsoluteHardwareKnob("KNOB%d %d-%d".formatted(deviceIndex, (i / 4) + 1, (i % 4) + 1)); + control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(BASE_CHANNEL, 0x4 + i)); + knobs.add(control); + + final XoneRgbButton button = + new XoneRgbButton( + i, i, "KNOB_BUTTON%d".formatted(deviceIndex), MidiStatus.NOTE_ON, ENCODER_BUTTONS_INDEX[i], + BASE_CHANNEL, surface, midiProcessor); + knobButtons.add(button); + } + this.layerEncoder = + new XoneEncoder( + 0, -1, BASE_CHANNEL, 0x14, 0x0D, "LAYER ENCODER%d".formatted(deviceIndex), midiProcessor, surface); + this.layerButton = globalStates.usesLayers() + ? null + : new XoneRgbButton( + 0, 0x1C, "LAYER%d".formatted(deviceIndex), MidiStatus.NOTE_ON, 0xC, BASE_CHANNEL, + surface, midiProcessor); + this.shiftEncoder = + new XoneEncoder( + 0, -1, BASE_CHANNEL, 0x15, 0x0E, "SHIFT ENCODER%d".formatted(deviceIndex), midiProcessor, + surface); + this.shiftButton = + new XoneRgbButton( + 0, 0x1D, "SHIFT%d".formatted(deviceIndex), MidiStatus.NOTE_ON, 0xF, BASE_CHANNEL, surface, + midiProcessor); + + if (globalStates.usesLayers()) { + setupFreeAssignmentControls(deviceIndex, surface, midiProcessor, midiIn); + } + } + + private void setupFreeAssignmentControls(final int deviceIndex, final HardwareSurface surface, + final XoneMidiDevice midiProcessor, final MidiIn midiIn) { + final List assignButtons = new ArrayList<>(); + for (int layer = 0; layer < 2; layer++) { + final List assignEncoders = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + final String name = "ENCODER%d Layer %d - %d".formatted(deviceIndex, layer + 2, i + 1); + assignEncoders.add(createExtControlKnob(surface, midiIn, name, BASE_TOP_ENCODER_CC[layer] + i)); + assignButtons.add( + new XoneAssignButton(midiProcessor, surface, layer, 0x1E + i, ENCODER_BUTTON_BASE[layer] + i)); + } + assignEncoders.add( + createExtControlKnob( + surface, midiIn, "ENCODER%d Layer %d LAYER".formatted(deviceIndex, layer + 2), + BASE_BOTTOM_ENCODER_CC[layer])); + assignEncoders.add( + createExtControlKnob( + surface, midiIn, "ENCODER%d Layer %d SCROLL".formatted(deviceIndex, layer + 2), + BASE_BOTTOM_ENCODER_CC[layer] + 1)); + + //XoneK3ControllerExtension.println("-----top %d-------", layer); + for (int i = 0; i < 12; i++) { + final int ledIndex = (2 - (i / 4)) * 4 + (i % 4); + assignButtons.add( + new XoneAssignButton(midiProcessor, surface, layer, ledIndex, LAYER_BUTTON_BASE[layer] + i + 0x10)); + } + //XoneK3ControllerExtension.println("------bottom %d ------", layer); + for (int i = 0; i < 16; i++) { + final int ledIndex = (3 - (i / 4)) * 4 + (i % 4) + 12; + assignButtons.add( + new XoneAssignButton(midiProcessor, surface, layer, ledIndex, LAYER_BUTTON_BASE[layer] + i)); + } + } + midiProcessor.setAssignButtons(assignButtons); + } + + public void disableKnobButtonSection(final Layer layer) { + getKnobButtons().forEach(button -> button.bindDisabled(layer)); + getKnobs().forEach(knob -> layer.bind(knob, EMPTY_CONSUMER)); + } + + public XoneRgbButton getLayerButton() { + return layerButton; + } + + public List getGridButtons() { + return gridButtons; + } + + public List getSliders() { + return sliders; + } + + public List getKnobs() { + return knobs; + } + + public List getKnobButtons() { + return knobButtons; + } + + public List getEncoders() { + return encoders; + } + + public XoneEncoder getShiftEncoder() { + return shiftEncoder; + } + + public XoneEncoder getLayerEncoder() { + return layerEncoder; + } + + public XoneRgbButton getShiftButton() { + return shiftButton; + } + + private RelativeHardwareKnob createExtControlKnob(final HardwareSurface surface, final MidiIn midiIn, + final String name, final int midiCcBase) { + final RelativeHardwareKnob hwEncoder = surface.createRelativeHardwareKnob(name); + hwEncoder.setAdjustValueMatcher(midiIn.createRelative2sComplementCCValueMatcher(0xE, midiCcBase, 40)); + hwEncoder.setStepSize(0.025); + return hwEncoder; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/TrackSpecControl.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/TrackSpecControl.java new file mode 100644 index 00000000..7d8420ac --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/TrackSpecControl.java @@ -0,0 +1,127 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +import java.util.UUID; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorDevice; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.DeviceMatcher; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.SpecificBitwigDevice; +import com.bitwig.extension.controller.api.Track; + +public class TrackSpecControl { + private static final UUID DJ_EQ_DEVICE_ID = UUID.fromString("3cc1b71a-e22a-42cf-89f0-316475368fb3"); + private static final UUID LIMITER_ID = UUID.fromString("8da7251e-2578-4bcc-b3c4-8f4ec2e115d0"); + + private final Track track; + private final Device djEqDevice; + private final Device limiterDevice; + private boolean djEqExists; + private boolean djEqActive; + private final Parameter lowGain; + private final Parameter midGain; + private final Parameter hiGain; + private final Parameter lowKill; + private final Parameter midKill; + private final Parameter hiKill; + private final CursorDevice cursorDevice; + private final Parameter lowFreq; + private final Parameter hiFreq; + private final CursorRemoteControlsPage trackRemotes; + private boolean trackExists = false; + + public TrackSpecControl(final int index, final Track track, final ControllerHost host) { + this.track = track; + cursorDevice = track.createCursorDevice(); + track.exists().addValueObserver(exists -> this.trackExists = exists); + + trackRemotes = track.createCursorRemoteControlsPage(8); + djEqDevice = createSpecDevice(host.createBitwigDeviceMatcher(DJ_EQ_DEVICE_ID)); + limiterDevice = createSpecDevice(host.createBitwigDeviceMatcher(LIMITER_ID)); + djEqDevice.exists().addValueObserver(exists -> this.djEqExists = exists); + djEqDevice.isEnabled().addValueObserver(active -> this.djEqActive = active); + + // CONTENTS/LOW_FREQ, CONTENTS/HIGH_FREQ, + final SpecificBitwigDevice djSpecificDevice = djEqDevice.createSpecificBitwigDevice(DJ_EQ_DEVICE_ID); + lowGain = djSpecificDevice.createParameter("LOW_GAIN"); + lowKill = djSpecificDevice.createParameter("KILL_LOWS"); + midGain = djSpecificDevice.createParameter("MID_GAIN"); + midKill = djSpecificDevice.createParameter("KILL_MIDS"); + hiGain = djSpecificDevice.createParameter("HIGH_GAIN"); + hiKill = djSpecificDevice.createParameter("KILL_HIGHS"); + lowFreq = djSpecificDevice.createParameter("LOW_FREQ"); + hiFreq = djSpecificDevice.createParameter("HIGH_FREQ"); + lowKill.value().markInterested(); + midKill.value().markInterested(); + hiKill.value().markInterested(); + // djEqDevice.addDirectParameterIdObserver(parmId -> { + // XoneK3ControllerExtension.println(" >> %s", Arrays.toString(parmId)); + // }); + } + + + public CursorRemoteControlsPage getTrackRemotes() { + return trackRemotes; + } + + private Device createSpecDevice(final DeviceMatcher matcher) { + final DeviceBank deviceBank = this.track.createDeviceBank(1); + deviceBank.setDeviceMatcher(matcher); + return deviceBank.getDevice(0); + } + + public boolean isTrackExists() { + return trackExists; + } + + public boolean isDjEqActive() { + return djEqActive; + } + + public void toggleDjEqActive() { + djEqDevice.isEnabled().toggle(); + } + + public Parameter getLowGain() { + return lowGain; + } + + public Parameter getMidGain() { + return midGain; + } + + public Parameter getHiGain() { + return hiGain; + } + + public Parameter getHiKill() { + return hiKill; + } + + public Parameter getMidKill() { + return midKill; + } + + public Parameter getLowKill() { + return lowKill; + } + + public boolean eqExists() { + return djEqExists; + } + + public Parameter getLowFreq() { + return lowFreq; + } + + public Parameter getHiFreq() { + return hiFreq; + } + + public void insertEq() { + cursorDevice.deviceChain().endOfDeviceChainInsertionPoint().insertBitwigDevice(DJ_EQ_DEVICE_ID); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/ViewControl.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/ViewControl.java new file mode 100644 index 00000000..dd165cfc --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/ViewControl.java @@ -0,0 +1,107 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.Project; +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 ViewControl { + + private final Track rootTrack; + private final TrackBank trackBank; + private final OverviewGrid overviewGrid; + private final TrackBank maxTrackBank; + private final CursorTrack cursorTrack; + private final PinnableCursorDevice cursorDevice; + private final Project project; + private final CursorRemoteControlsPage deviceRemotePages; + private final CursorRemoteControlsPage projectRemotes; + private final CursorRemoteControlsPage trackRemotes; + private final List specControls = new ArrayList<>(); + + public ViewControl(final ControllerHost host, final XoneK3GlobalStates globalStates) { + rootTrack = host.getProject().getRootTrackGroup(); + // Create Track Bank Params // Num Track | Num Sends | Num Scenes | has Flat List + trackBank = host.createTrackBank(4 * globalStates.getDeviceCount(), 5, 4, true); + maxTrackBank = host.createTrackBank(64, 1, 64, false); + overviewGrid = new OverviewGrid(maxTrackBank); + cursorTrack = host.createCursorTrack(2, 1); + trackBank.followCursorTrack(cursorTrack); + cursorDevice = cursorTrack.createCursorDevice(); + project = host.getProject(); + deviceRemotePages = cursorDevice.createCursorRemoteControlsPage(8); + final Track rootTrack = project.getRootTrackGroup(); + projectRemotes = rootTrack.createCursorRemoteControlsPage(8); + trackRemotes = cursorTrack.createCursorRemoteControlsPage(8); + + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final Track track = trackBank.getItemAt(i); + prepareTrack(track); + specControls.add(new TrackSpecControl(i, track, host)); + } + overviewGrid.setUpFocusScene(trackBank); + } + + public TrackBank getTrackBank() { + return trackBank; + } + + 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(); + track.isStopped().markInterested(); + track.isQueuedForStop().markInterested(); + } + + public Track getRootTrack() { + return rootTrack; + } + + public List getSpecControls() { + return specControls; + } + + public PinnableCursorDevice getCursorDevice() { + return cursorDevice; + } + + public boolean hasQueuedClips(final int sceneIndex) { + return overviewGrid.hasQueuedScenes(sceneIndex); + } + + public boolean hasPlayingClips(final int sceneIndex) { + return overviewGrid.hasPlayingClips(sceneIndex); + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public CursorRemoteControlsPage getTrackRemotes() { + return trackRemotes; + } + + public CursorRemoteControlsPage getDeviceRemotePages() { + return deviceRemotePages; + } + + public CursorRemoteControlsPage getProjectRemotes() { + return projectRemotes; + } + +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneHwElements.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneHwElements.java new file mode 100644 index 00000000..c0cc50ed --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneHwElements.java @@ -0,0 +1,44 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneRgbButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class XoneHwElements { + + private final List hwElements = new ArrayList<>(); + private final int deviceCount; + + public XoneHwElements(final HardwareSurface surface, final XoneMidiProcessor midiProcessor, + final XoneK3GlobalStates globalStates) { + deviceCount = globalStates.getDeviceCount(); + for (int i = 0; i < globalStates.getDeviceCount(); i++) { + hwElements.add(new DeviceHwElements(i, surface, midiProcessor.getMidiDevice(i), globalStates)); + } + } + + public DeviceHwElements getDeviceElements(final int index) { + return hwElements.get(index); + } + + public XoneRgbButton getGridButton(final int trackIndex, final int sceneIndex) { + final int deviceIndex = trackIndex / 4; + return hwElements.get(deviceIndex).getGridButtons().get(sceneIndex * 4 + (trackIndex % 4)); + } + + public void disableKnobButtonSectionRightSide(final Layer layer) { + if (deviceCount == 1) { + return; + } + for (int i = 1; i < deviceCount; i++) { + hwElements.get(i).disableKnobButtonSection(layer); + } + + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ControllerExtension.java new file mode 100644 index 00000000..9d2a7121 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ControllerExtension.java @@ -0,0 +1,268 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.ControllerExtension; +import com.bitwig.extension.controller.ControllerExtensionDefinition; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneRgbColor; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneEncoder; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneRgbButton; +import com.bitwig.extensions.controllers.allenheath.xonek3.layer.GridMode; +import com.bitwig.extensions.controllers.allenheath.xonek3.layer.LayerCollection; +import com.bitwig.extensions.controllers.allenheath.xonek3.layer.LayerId; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.BooleanValueObject; +import com.bitwig.extensions.framework.values.EnumeratorValue; + +public class XoneK3ControllerExtension extends ControllerExtension { + + private static ControllerHost debugHost; + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + private HardwareSurface surface; + private Context diContext; + + private final EnumeratorValue mixerMode = new EnumeratorValue<>(LayerMode.values()); + private final EnumeratorValue gridMode = new EnumeratorValue<>(GridMode.values()); + + private LayerCollection layerCollection; + private final XoneK3GlobalStates globalStates; + private ViewControl viewControl; + private Parameter tempo; + + private enum LayerMode { + MIXER(XoneRgbColor.WHITE), + TRACK_INDIVIDUAL(XoneRgbColor.WHITE), // XoneRgbColor.PURPLE + DJ_EQ(XoneRgbColor.WHITE), // XoneRgbColor.MAGENTA + DEVICE(XoneRgbColor.ORANGE_REMOTES), // XoneRgbColor.BLUE + TRACK_REMOTES(XoneRgbColor.ORANGE_REMOTES), // XoneRgbColor.ORANGE + PROJECT_REMOTES(XoneRgbColor.ORANGE_REMOTES); // XoneRgbColor.YELLOW + + private final XoneRgbColor color; + private final XoneRgbColor dimColor; + + LayerMode(final XoneRgbColor color) { + this.color = color; + this.dimColor = color.bright(3); + } + } + + 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 XoneK3ControllerExtension(final ControllerExtensionDefinition definition, final ControllerHost host, + final int deviceCount) { + super(definition, host); + globalStates = new XoneK3GlobalStates(host, deviceCount); + } + + @Override + public void init() { + debugHost = getHost(); + diContext = new Context(this); + diContext.registerService(XoneK3GlobalStates.class, globalStates); + surface = diContext.getService(HardwareSurface.class); + layerCollection = diContext.getService(LayerCollection.class); + viewControl = diContext.getService(ViewControl.class); + final Transport transport = diContext.getService(Transport.class); + tempo = transport.tempo(); + tempo.value().markInterested(); + for (int i = 0; i < globalStates.getDeviceCount(); i++) { + initModifiers(layerCollection.getLayer(LayerId.MAIN), i); + initMixerControl(layerCollection.getLayer(LayerId.MIXER), i); + } + diContext.activate(); + globalStates.activate(); + diContext.getService(XoneMidiProcessor.class).init(); + } + + private void initModifiers(final Layer mainLayer, final int deviceIndex) { + final DeviceHwElements hwElements = diContext.getService(XoneHwElements.class).getDeviceElements(deviceIndex); + final XoneRgbButton shiftButton = hwElements.getShiftButton(); + shiftButton.bindIsPressed(mainLayer, pressed -> globalStates.getShiftHeld().set(pressed)); + shiftButton.bindLight( + mainLayer, + () -> globalStates.getShiftHeld().get() ? XoneRgbColor.WHITE : XoneRgbColor.WHITE_LO); + + final Layer layerChooserLayer = layerCollection.getLayer(LayerId.LAYER_CHOOSER); + final XoneRgbButton scrollEncoderButton = hwElements.getShiftEncoder().getPushButton(); + final Layer gridModeSelectionLayer = layerCollection.getLayer(LayerId.GRID_LAYER_CHOOSER); + final XoneEncoder layerEncoder = hwElements.getLayerEncoder(); + final XoneRgbButton layerEncoderButton = layerEncoder.getPushButton(); + mixerMode.addValueObserver(newMode -> applyMixerMode()); + + if (globalStates.usesLayers()) { + final BooleanValueObject gridModeLayerActive = new BooleanValueObject(); + final int layerEncoderIndex = 3; + final XoneEncoder layerSelectionEncoder = hwElements.getEncoders().get(layerEncoderIndex); + final XoneRgbButton layerButton = layerSelectionEncoder.getPushButton(); + layerButton.bindIsPressed(mainLayer, layerChooserLayer::setIsActive); + layerEncoderButton.bindIsPressed( + mainLayer, pressed -> gridModeLayerActive.set(pressed && scrollEncoderButton.isPressed().get())); + scrollEncoderButton.isPressed() + .addValueObserver(pressed -> gridModeLayerActive.set(pressed && layerEncoderButton.isPressed().get())); + bindEncoderLayerForLayerMode(hwElements, layerChooserLayer, layerEncoderIndex); + gridModeLayerActive.addValueObserver(gridModeSelectionLayer::setIsActive); + bindGridModeSelector(hwElements, gridModeSelectionLayer); + } else { + final Layer sceneLaunchlayer = layerCollection.getLayer(LayerId.SCENE_LAUNCHER); + layerEncoder.bindEncoder( + mainLayer, createIncrementBinder(inc -> handleMainLayerEncoder(inc, layerEncoderButton))); + final XoneRgbButton layerButton = hwElements.getLayerButton(); + layerButton.bindLightPressed(mainLayer, XoneRgbColor.WHITE, XoneRgbColor.WHITE_LO); + layerButton.bindIsPressed( + mainLayer, pressed -> { + sceneLaunchlayer.setIsActive(pressed); + layerChooserLayer.setIsActive(pressed); + }); + if (deviceIndex == 0) { + bindUserSelectionModes(hwElements, layerChooserLayer); + } else { + hwElements.disableKnobButtonSection(layerChooserLayer); + } + } + } + + private void bindUserSelectionModes(final DeviceHwElements hwElements, final Layer layer) { + final List buttons = hwElements.getKnobButtons(); + bindModeButton(buttons.get(0), layer, LayerMode.MIXER); + bindModeButton(buttons.get(1), layer, LayerMode.TRACK_INDIVIDUAL); + bindModeButton(buttons.get(2), layer, LayerMode.DJ_EQ); + bindModeButton(buttons.get(4), layer, LayerMode.DEVICE); + bindModeButton(buttons.get(5), layer, LayerMode.TRACK_REMOTES); + bindModeButton(buttons.get(6), layer, LayerMode.PROJECT_REMOTES); + bindEmptyButton(buttons.get(3), layer); + bindEmptyButton(buttons.get(7), layer); + } + + private void bindModeButton(final XoneRgbButton button, final Layer layer, final LayerMode mode) { + button.bindLight(layer, () -> mixerMode.get() == mode ? mode.color : mode.dimColor); + button.bindPressed(layer, () -> mixerMode.set(mode)); + } + + private void bindEmptyButton(final XoneRgbButton button, final Layer layer) { + button.bindLight(layer, () -> XoneRgbColor.OFF); + button.bindPressed(layer, () -> {}); + } + + private void bindGridModeSelector(final DeviceHwElements hwElements, final Layer layer) { + final List buttons = hwElements.getGridButtons(); + gridMode.addValueObserver(this::handleGridModeChange); + final GridMode[] values = GridMode.values(); + for (int i = 0; i < values.length; i++) { + final GridMode mode = values[i]; + final XoneRgbButton button = buttons.get(i); + button.bindPressed(layer, () -> gridMode.set(mode)); + button.bindLight(layer, () -> gridMode.get() == mode ? XoneRgbColor.BLUE : XoneRgbColor.WHITE_DIM); + } + for (int i = values.length; i < 16; i++) { + final XoneRgbButton button = buttons.get(i); + button.bindPressed(layer, () -> {}); + button.bindLight(layer, () -> XoneRgbColor.OFF); + } + } + + private void handleGridModeChange(final GridMode mode) { + final GridMode[] values = GridMode.values(); + for (final GridMode modeFromValue : values) { + layerCollection.getLayer(modeFromValue.getLayerId()).setIsActive(mode == modeFromValue); + } + } + + private void handleMainLayerEncoder(final int inc, final XoneRgbButton button) { + if (button.isPressed().get()) { + tempo.incRaw(inc * (globalStates.getShiftHeld().get() ? 1.0 : 0.1)); + } else { + if (inc > 0) { + viewControl.getCursorTrack().selectNext(); + } else { + viewControl.getCursorTrack().selectPrevious(); + } + } + } + + private void bindEncoderLayerForLayerMode(final DeviceHwElements hwElements, final Layer layer, + final int layerEncoderIndex) { + final XoneEncoder layerEncoder = hwElements.getEncoders().get(layerEncoderIndex); + layerEncoder.bindEncoder(layer, createIncrementBinder(inc -> mixerMode.increment(inc, false))); + layerEncoder.getPushButton().bindLight(layer, this::getKnobRow12Mode); + for (int i = 0; i < 4; i++) { + if (i != layerEncoderIndex) { + final XoneEncoder encoder = hwElements.getEncoders().get(i); + final XoneRgbButton encoderButton = encoder.getPushButton(); + encoderButton.bindLight(layer, () -> XoneRgbColor.OFF); + } + } + } + + private InternalHardwareLightState getKnobRow12Mode() { + return switch (mixerMode.get()) { + case MIXER -> XoneRgbColor.WHITE; + case DEVICE -> XoneRgbColor.BLUE; + case DJ_EQ -> XoneRgbColor.MAGENTA; + case TRACK_INDIVIDUAL -> XoneRgbColor.PURPLE; + case TRACK_REMOTES -> XoneRgbColor.ORANGE; + case PROJECT_REMOTES -> XoneRgbColor.YELLOW; + }; + } + + private void applyMixerMode() { + layerCollection.setActive(LayerId.REMOTES, mixerMode.get() == LayerMode.DEVICE); + layerCollection.setActive(LayerId.DJ_EQ, mixerMode.get() == LayerMode.DJ_EQ); + layerCollection.setActive(LayerId.TRACK_REMOTES, mixerMode.get() == LayerMode.TRACK_REMOTES); + layerCollection.setActive(LayerId.PROJECT_REMOTES, mixerMode.get() == LayerMode.PROJECT_REMOTES); + layerCollection.setActive(LayerId.IND_REMOTES, mixerMode.get() == LayerMode.TRACK_INDIVIDUAL); + } + + private void initMixerControl(final Layer layer, final int deviceIndex) { + final DeviceHwElements hwElements = diContext.getService(XoneHwElements.class).getDeviceElements(deviceIndex); + final ViewControl viewControl = diContext.getService(ViewControl.class); + final TrackBank trackBank = viewControl.getTrackBank(); + final XoneEncoder navigateEncoder = hwElements.getShiftEncoder(); + final XoneRgbButton button = navigateEncoder.getPushButton(); + navigateEncoder.bindEncoder(layer, createTrackScrollingBinder(button, trackBank)); + } + + private RelativeHardwarControlBindable createTrackScrollingBinder(final XoneRgbButton button, + final TrackBank trackBank) { + return createIncrementBinder(inc -> { + if (button.isPressed().get()) { + trackBank.sceneBank().scrollBy(inc); + } else { + trackBank.scrollBy(inc); + } + }); + } + + @Override + public void exit() { + diContext.deactivate(); + } + + @Override + public void flush() { + surface.updateHardware(); + } + + public RelativeHardwarControlBindable createIncrementBinder(final IntConsumer consumer) { + return getHost().createRelativeHardwareControlStepTarget(// + getHost().createAction(() -> consumer.accept(1), () -> "+"), + getHost().createAction(() -> consumer.accept(-1), () -> "-")); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ExtensionDefinition.java new file mode 100644 index 00000000..21cd88d6 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ExtensionDefinition.java @@ -0,0 +1,82 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +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 XoneK3ExtensionDefinition extends ControllerExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("547d4e10-389a-492c-b7d7-a2c3c45df90e"); + + public XoneK3ExtensionDefinition() { + } + + @Override + public String getName() { + return "Xone:K3"; + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "0.9"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareVendor() { + return "Allen & Heath"; + } + + @Override + public String getHardwareModel() { + return "Xone:K3"; + } + + @Override + public int getRequiredAPIVersion() { + return 24; + } + + @Override + public int getNumMidiInPorts() { + return 1; + } + + @Override + public int getNumMidiOutPorts() { + return 1; + } + + @Override + public String getHelpFilePath() { + return "Controllers/AllenHeath/Allen & Heath Xone K3.pdf"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS || platformType == PlatformType.MAC) { + list.add(new String[] {"XONE:K3"}, new String[] {"XONE:K3"}); + list.add(new String[] {"XONE:K3 Bitwig"}, new String[] {"XONE:K3 Bitwig"}); + } else { + list.add(new String[] {"XONE:K3 MIDI 1"}, new String[] {"XONE:K3 MIDI 1"}); + list.add(new String[] {"XONE:K3 MIDI 1 Bitwig"}, new String[] {"XONE:K3 MIDI 1 Bitwig"}); + } + } + + @Override + public XoneK3ControllerExtension createInstance(final ControllerHost host) { + return new XoneK3ControllerExtension(this, host, 1); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ExtensionX2Definition.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ExtensionX2Definition.java new file mode 100644 index 00000000..3dff768c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3ExtensionX2Definition.java @@ -0,0 +1,85 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +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 XoneK3ExtensionX2Definition extends ControllerExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("547d4e10-389a-492c-b7d7-a2c3c45df90f"); + + public XoneK3ExtensionX2Definition() { + } + + @Override + public String getName() { + return "Xone:K3 x2"; + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "0.9"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareVendor() { + return "Allen & Heath"; + } + + @Override + public String getHardwareModel() { + return "Xone:K3 x2"; + } + + @Override + public int getRequiredAPIVersion() { + return 24; + } + + @Override + public int getNumMidiInPorts() { + return 2; + } + + @Override + public int getNumMidiOutPorts() { + return 2; + } + + @Override + public String getHelpFilePath() { + return "Controllers/AllenHeath/Allen & Heath Xone K3.pdf"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS || platformType == PlatformType.MAC) { + list.add(new String[] {"XONE:K3", "XONE:K3 #2"}, new String[] {"XONE:K3", "XONE:K3 #2"}); + list.add(new String[] {"XONE:K3 Bitwig", "XONE:K3"}, new String[] {"XONE:K3 Bitwig", "XONE:K3"}); + } else { + list.add( + new String[] {"XONE:K3 MIDI 1", "XONE:K3 MIDI 2"}, new String[] {"XONE:K3 MIDI 1", "XONE:K3 MIDI 2"}); + list.add( + new String[] {"XONE:K3 MIDI 1 Bitwig", "XONE:K3 MIDI 2"}, + new String[] {"XONE:K3 MIDI 1 Bitwig", "XONE:K3 MIDI 2"}); + } + } + + @Override + public XoneK3ControllerExtension createInstance(final ControllerHost host) { + return new XoneK3ControllerExtension(this, host, 2); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3GlobalStates.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3GlobalStates.java new file mode 100644 index 00000000..cb0ca35a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneK3GlobalStates.java @@ -0,0 +1,71 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneRgbColor; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +public class XoneK3GlobalStates { + private final BooleanValueObject shiftHeld = new BooleanValueObject(); + private final BooleanValueObject layerHeld = new BooleanValueObject(); + private boolean usesLayers = false; + private int blinkState = 0; + private final ControllerHost host; + private final int deviceCount; + + public XoneK3GlobalStates(final ControllerHost host, final int deviceCount) { + this.host = host; + this.deviceCount = deviceCount; + } + + public void activate() { + host.scheduleTask(this::handlePing, 100); + } + + public int getDeviceCount() { + return deviceCount; + } + + private void handlePing() { + blinkState = (blinkState + 1) % 128; + host.scheduleTask(this::handlePing, 40); + } + + public BooleanValueObject getShiftHeld() { + return shiftHeld; + } + + public BooleanValueObject getLayerHeld() { + return layerHeld; + } + + public void setUsesLayers(final boolean usesLayers) { + this.usesLayers = usesLayers; + } + + public boolean usesLayers() { + return usesLayers; + } + + public XoneRgbColor blinkFast(final XoneRgbColor color) { + if (blinkState % 4 < 2) { + return color; + } + return XoneRgbColor.OFF; + } + + public XoneRgbColor pulse(final XoneRgbColor color) { + if (blinkState % 16 < 14) { + return color; + } + return XoneRgbColor.OFF; + } + + public XoneRgbColor blinkMid(final XoneRgbColor color) { + if (blinkState % 8 < 4) { + return color; + } + return XoneRgbColor.OFF; + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneMidiDevice.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneMidiDevice.java new file mode 100644 index 00000000..a933b16d --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneMidiDevice.java @@ -0,0 +1,230 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.ColorIndexCell; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneRgbColor; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneAssignButton; +import com.bitwig.extensions.framework.values.MidiStatus; + +public class XoneMidiDevice { + public static final String IN_SYSEX_SETUP_HEADER = "f000001a5015"; + + private final int deviceIndex; + private final MidiIn midiIn; + private final MidiOut midiOut; + private final List colorIndexCells = new ArrayList<>(); + private final XoneMidiProcessor.InternalRgbColor[] colorStore = new XoneMidiProcessor.InternalRgbColor[34]; + private final int layerMode = 0; + private final List assignButtons = new ArrayList<>(); + + private final byte[] ledUpdateData = new byte[] { + (byte) 0xF0, 0x00, 0x00, 0x1A, 0x50, 0x15, 0x04, 0x7F, 0x7F, 0x7F, 0x10, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, + 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xF7 + }; + + private final byte[] globalLedUpdateData = new byte[] { + (byte) 0xF0, 0x00, 0x00, 0x1A, 0x50, 0x15, 0x00, 0x0E, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, (byte) 0xF7 + }; + + private final byte[] globalLedUpdateDataOverride = new byte[] { + (byte) 0xF0, 0x00, 0x00, 0x1A, 0x50, 0x15, 0x00, 0x0E, 0x7F, (byte) 0xF7 + }; + + private final byte[] switchUpdateData = new byte[] { + (byte) 0xF0, 0x00, 0x00, 0x1A, 0x50, 0x15, 0x01, // + 0x7F, // 07 - type = index + 0x7F, // 08 - layer + 0x10, // 09 - channel + 0x00, // 10 - Mode always Gate + 0x01, // 11 - Command = 0x1 Chromatic Note + 0x7F, // 12 - MIDI/CC Nr + 0x00, 0x00, // 14 - Min + 0x00, 0x7F, // 16 - Max + 0x00, 0x00, // 18 - Step + 0x00, 0x00, 0x00, (byte) 0xF7 + }; + + private final byte[] analogUpdateData = new byte[] { + (byte) 0xF0, 0x00, 0x00, 0x1A, 0x50, 0x15, 0x02, // + 0x7F, // 07 - type = index + 0x7F, // 08 - layer + 0x10, // 09 - channel + 0x00, // 10 - Curve -> Complicated + 0x05, // 11 - Command = 0x5 CC Value + 0x7F, // 12 - CC Nr + 0x00, 0x00, // 14 - Min + 0x00, 0x7F, // 16 - Max + 0x00, 0x00, // 18 - Step + 0x00, 0x00, 0x00, (byte) 0xF7 + }; + + private final byte[] encoderUpdateData = new byte[] { + (byte) 0xF0, 0x00, 0x00, 0x1A, 0x50, 0x15, 0x03, // + 0x7F, // 07 - type = index + 0x7F, // 08 - layer + 0x10, // 09 - channel + 0x00, // 10 - Mode -> 0x00 always Relative + 0x05, // 11 - Command = 0x5 CC Value + 0x7F, // 12 - CC Nr + 0x00, 0x00, // 14 - Min + 0x00, 0x7F, // 16 - Max + 0x00, 0x00, // 18 - Step + 0x00, 0x00, 0x00, (byte) 0xF7 + }; + + + public XoneMidiDevice(final int deviceIndex, final MidiIn midiIn, final MidiOut midiOut) { + this.midiIn = midiIn; + this.midiOut = midiOut; + this.deviceIndex = deviceIndex; + midiIn.setSysexCallback(this::handleSysEx); + for (int i = 0; i < colorStore.length; i++) { + colorStore[i] = new XoneMidiProcessor.InternalRgbColor(); + } + for (int layer = 0; layer < 2; layer++) { + for (int i = 0; i < 34; i++) { + colorIndexCells.add(new ColorIndexCell(layer + 1, i)); + } + } + } + + public int getDeviceIndex() { + return deviceIndex; + } + + public void init() { + midiOut.sendSysex("F0 00 00 1A 50 15 00 0D 00 F7"); // Turn Latching Layers off + midiOut.sendSysex("F0 00 00 1A 50 15 00 05 0E F7"); // Global Channel to 15 + } + + public void setAssignButtons(final List assignButtons) { + this.assignButtons.clear(); + this.assignButtons.addAll(assignButtons); + for (final XoneAssignButton button : assignButtons) { + getColorCell(button.getLayerIndex(), button.getLedIndex()).ifPresent( + cell -> cell.setNoteCcValue(button.getMidiNr())); + } + } + + private void handleMidiIn(final int status, final int data1, final int data2) { + XoneK3ControllerExtension.println(" MIDI in %02X %02X %02X", status, data1, data2); + } + + private void handleSysEx(final String data) { + if (data.startsWith(IN_SYSEX_SETUP_HEADER)) { + readLedConfiguration(data); + } else if (data.contains(IN_SYSEX_SETUP_HEADER)) { + final String ext = data.substring(data.indexOf(IN_SYSEX_SETUP_HEADER)); + readLedConfiguration(ext); + } else { + XoneK3ControllerExtension.println(" CODE = %s", data); + } + } + + private void readLedConfiguration(final String data) { + final int component = extract(data, 6); + final int elementIndex = extract(data, 7); + final int layerIndex = extract(data, 8); + if (component == 4 && layerIndex > 0 && layerIndex < 3 && elementIndex < 34) { + getColorCell(layerIndex - 1, elementIndex).ifPresent(cell -> { + final int colorPaletteIndex = extract(data, 9); + cell.setColorValue(colorPaletteIndex); + cell.setColor(XoneRgbColor.getPaletteColor(colorPaletteIndex)); + }); + } + } + + private Optional getColorCell(final int layer, final int ledIndex) { + final int index = layer * 34 + ledIndex; + if (index < colorIndexCells.size()) { + return Optional.of(colorIndexCells.get(index)); + } + return Optional.empty(); + } + + + private int extract(final String data, final int byteIndex) { + if (byteIndex * 2 + 2 < data.length()) { + return Integer.parseInt(data.substring(byteIndex * 2, byteIndex * 2 + 2), 16); + } + return -1; + } + + public void configureLed(final int index, final int layer, final int color, final MidiStatus midiStatus, + final int noteCcNr) { + ledUpdateData[7] = (byte) (index & 0x7F); + ledUpdateData[8] = (byte) (layer & 0x7F); + ledUpdateData[9] = (byte) (color & 0x7F); + ledUpdateData[12] = (byte) (midiStatus == MidiStatus.NOTE_ON ? 0x01 : 0x05); + ledUpdateData[13] = (byte) (noteCcNr & 0x7F); + midiOut.sendSysex(ledUpdateData); + pause(3); + } + + public void updateLed(final int index, final int red, final int green, final int blue, final int brightness) { + colorStore[index].set(red, green, blue, brightness); + if (layerMode == 0) { + sendColor(index, colorStore[index]); + } + } + + public void sendColor(final int index, final XoneRgbColor color, final int brightness) { + globalLedUpdateData[8] = (byte) (index & 0x7F); + globalLedUpdateData[9] = (byte) (color.getRed() & 0x7F); + globalLedUpdateData[10] = (byte) (color.getGreen() & 0x7F); + globalLedUpdateData[11] = (byte) (color.getBlue() & 0x7F); + globalLedUpdateData[12] = (byte) (brightness & 0x7F); + midiOut.sendSysex(globalLedUpdateData); + pause(3); + } + + public void sendColor(final int index, final XoneMidiProcessor.InternalRgbColor color) { + globalLedUpdateData[8] = (byte) (index & 0x7F); + globalLedUpdateData[9] = (byte) (color.red & 0x7F); + globalLedUpdateData[10] = (byte) (color.green & 0x7F); + globalLedUpdateData[11] = (byte) (color.blue & 0x7F); + globalLedUpdateData[12] = (byte) (color.brightness & 0x7F); + midiOut.sendSysex(globalLedUpdateData); + pause(3); + } + + public void clearLedOverride(final int index) { + globalLedUpdateDataOverride[8] = (byte) (index & 0x7F); + midiOut.sendSysex(globalLedUpdateDataOverride); + } + + public MidiIn getMidiIn() { + return midiIn; + } + + public void sendMidi(final int status, final int val1, final int val2) { + midiOut.sendMidi(status, val1, val2); + } + + public void updateAssignLed(final int midiStatus, final int midiNr, final int layerIndex, final int ledIndex, + final boolean isOn) { + sendMidi(midiStatus, midiNr, isOn ? 0x7F : 0x00); + getColorCell(layerIndex, ledIndex).ifPresent(cell -> { + cell.setOn(isOn); + if (cell.getLayer() == layerMode) { + sendColor(cell.getIndex(), isOn ? cell.getColor() : XoneRgbColor.OFF, 0x20); + } + }); + } + + + private static void pause(final int mstime) { + try { + Thread.sleep(mstime); + } + catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneMidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneMidiProcessor.java new file mode 100644 index 00000000..e9d66499 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/XoneMidiProcessor.java @@ -0,0 +1,91 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.time.TimedEvent; + +@Component +public class XoneMidiProcessor { + private final ControllerHost host; + private int layerMode = 0; + private final List modeChangeListeners = new ArrayList<>(); + protected final Queue timedEvents = new ConcurrentLinkedQueue<>(); + private final List midiDevices = new ArrayList<>(); + private final boolean usesLayer; + + protected static class InternalRgbColor { + protected int red; + protected int green; + protected int blue; + protected int brightness; + + public InternalRgbColor() { + } + + public void set(final int red, final int green, final int blue, final int brightness) { + this.red = red; + this.green = green; + this.blue = blue; + this.brightness = brightness; + } + } + + + public XoneMidiProcessor(final ControllerHost host, final XoneK3GlobalStates globalStates) { + this.host = host; + + for (int i = 0; i < globalStates.getDeviceCount(); i++) { + midiDevices.add(new XoneMidiDevice(i, host.getMidiInPort(i), host.getMidiOutPort(i))); + } + usesLayer = globalStates.usesLayers(); + // if (globalStates.usesLayers()) { + // midiIn.setMidiCallback(this::handleMidiInLayers); + // } else { + // midiIn.setMidiCallback(this::handleMidiIn); + // } + } + + public XoneMidiDevice getMidiDevice(final int index) { + return midiDevices.get(index); + } + + public void init() { + host.scheduleTask(() -> this.processMidi(), 100); + midiDevices.forEach(xoneMidiDevice -> xoneMidiDevice.init()); + // if (usesLayer) { + // midiOut.sendSysex("F0 00 00 1A 50 15 00 07 F7"); // Reqeust Set up + // } + } + + private void changeLayerMode(final int newMode) { + this.layerMode = newMode; + modeChangeListeners.forEach(l -> l.accept(layerMode)); + } + + public RelativeHardwarControlBindable createIncrementBinder(final IntConsumer consumer) { + return host.createRelativeHardwareControlStepTarget(// + host.createAction(() -> consumer.accept(1), () -> "+"), + host.createAction(() -> consumer.accept(-1), () -> "-")); + } + + + private void processMidi() { + if (!timedEvents.isEmpty()) { + for (final TimedEvent event : timedEvents) { + event.process(); + if (event.isCompleted()) { + timedEvents.remove(event); + } + } + } + host.scheduleTask(this::processMidi, 50); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/ColorIndexCell.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/ColorIndexCell.java new file mode 100644 index 00000000..8aa00acc --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/ColorIndexCell.java @@ -0,0 +1,82 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.color; + +import com.bitwig.extensions.framework.values.MidiStatus; + +public class ColorIndexCell { + private final byte[] ledUpdateData = new byte[] { + (byte) 0xF0, 0x00, 0x00, 0x1A, 0x50, 0x15, 0x04, 0x7F, 0x7F, 0x7F, 0x10, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, + 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xF7 + }; + + private final int index; + private final int layer; + private int colorValue; + private boolean on; + private final MidiStatus midiStatus; + private int noteCcValue = -1; + private XoneRgbColor color = XoneRgbColor.WHITE_DIM; + + public ColorIndexCell(final int layer, final int index) { + this.index = index; + this.layer = layer; + this.midiStatus = MidiStatus.NOTE_ON; + ledUpdateData[7] = (byte) (index & 0x7F); + ledUpdateData[8] = (byte) (layer & 0x7F); + } + + public MidiStatus getMidiStatus() { + return midiStatus; + } + + public int getNoteCcValue() { + return noteCcValue; + } + + public void setNoteCcValue(final int noteCcValue) { + this.noteCcValue = noteCcValue; + } + + public int getIndex() { + return index; + } + + public int getLayer() { + return layer; + } + + public int getColorValue() { + return colorValue; + } + + public void setColorValue(final int colorValue) { + this.colorValue = colorValue; + } + + public boolean isDefined() { + return noteCcValue != -1; + } + + public boolean isOn() { + return on; + } + + public void setOn(final boolean on) { + this.on = on; + } + + public XoneRgbColor getColor() { + return color; + } + + public void setColor(final XoneRgbColor color) { + this.color = color; + } + + public byte[] getLedUpdateData() { + ledUpdateData[9] = (byte) (colorValue & 0x7F); + ledUpdateData[12] = (byte) (midiStatus == MidiStatus.NOTE_ON ? 0x01 : 0x05); + ledUpdateData[13] = (byte) (noteCcValue & 0x7F); + return ledUpdateData; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/ColorLookup.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/ColorLookup.java new file mode 100644 index 00000000..97fffa4a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/ColorLookup.java @@ -0,0 +1,119 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.color; + +import java.util.List; + +import com.bitwig.extension.api.Color; + +public class ColorLookup { + 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 static final List COLOR_SEQ = List.of( // + new ColorMatch(0, 0, 0, 0), // + new ColorMatch(0, 0, 0, 0), // 0x1 Black + new ColorMatch(1, 255, 0, 0), // 0x2 RED + new ColorMatch(2, 255, 165, 0), // 0x3 ORANGE + new ColorMatch(2, 165, 42, 42), // 0x4 BROWN + new ColorMatch(3, 55, 255, 0), // 0x5 YELLOW + new ColorMatch(4, 44, 238, 144), // 0x6 Light GREEN + new ColorMatch(6, 0, 255, 0), // 0x7 GREEN + new ColorMatch(7, 244, 255, 255), // 0x8 Light Cyan + new ColorMatch(7, 0, 255, 255), // 0x9 Cyan + new ColorMatch(9, 173, 216, 230), // 0xA Light blue + new ColorMatch(8, 0, 0, 255), // 0xB Blue + new ColorMatch(11, 255, 0, 255), // 0xC Magenta + new ColorMatch(12, 255, 182, 193), // 0xD Light Pink + new ColorMatch(12, 255, 192, 203), // 0xE Pink + new ColorMatch(15, 255, 255, 255), // 0xF WHITE + // + new ColorMatch(3, 255, 255, 38), // 0x5 YELLOW + new ColorMatch(8, 134, 137, 172), // 0xA LIGHT BLUE + new ColorMatch(7, 87, 97, 198), // 0xB LIGHT BLUE + new ColorMatch(11, 149, 73, 203), // 0xC MAGENTA + new ColorMatch(3, 246, 246, 156), // 0x5 YELLOW + new ColorMatch(3, 219, 188, 28), // 0x5 YELLOW + new ColorMatch(2, 244, 168, 147), // 0x4 BROWN + new ColorMatch(2, 163, 121, 67), // 0x4 BROWN + new ColorMatch(6, 0, 157, 71), // 0x7 GREEN + new ColorMatch(6, 0, 166, 148), // 0x7 GREEN + new ColorMatch(6, 67, 210, 185), // 0x7 GREEN + new ColorMatch(1, 217, 46, 36), // 0x2 RED + new ColorMatch(1, 172, 35, 59), // 0x2 RED + new ColorMatch(1, 236, 97, 87), // 0x2 RED + new ColorMatch(2, 255, 87, 6), // 0x3 ORANGE + new ColorMatch(2, 236, 97, 87), // 0x3 ORANGE + new ColorMatch(2, 255, 131, 62), // 0x3 ORANGE + new ColorMatch(12, 134, 137, 172), // 0xE Pink + new ColorMatch(4, 115, 152, 20), // 0x6 Light GREEN + new ColorMatch(7, 68, 200, 255), // 0x9 Cyan + new ColorMatch(7, 0, 153, 217), // 0x9 Cyan + new ColorMatch(4, 160, 192, 76), // 0x6 Light GREEN + new ColorMatch(4, 139, 232, 184), // 0x6 Light GREEN + new ColorMatch(7, 88, 180, 186), // 0xA Light blue + new ColorMatch(11, 78, 0, 137), // 0xC Magenta + new ColorMatch(12, 225, 102, 145), // PINK + new ColorMatch(8, 90, 177, 245), // Light BLUE + new ColorMatch(8, 9, 156, 183), // Light BLUE + new ColorMatch(9, 90, 177, 245), // BLUE + new ColorMatch(9, 11, 115, 194), // BLUE + new ColorMatch(9, 14, 142, 240), // BLUE + // + new ColorMatch(12, 209, 185, 216) // BW PINK + ); + + private static final XoneIndexColor[] colorTable = new XoneIndexColor[16]; + + static { + for (int i = 0; i < 16; i++) { + colorTable[i] = new XoneIndexColor(i); + } + } + + public static XoneIndexColor toColor(final float r, final float g, final float b) { + return colorTable[rgbToIndex(r, g, b)]; + } + + public static XoneIndexColor toColor( + final float r, final float g, final float b, final int defaultIndex, final XoneIndexColor defaultColor) { + final int index = rgbToIndex(r, g, b); + if (index == defaultIndex) { + return defaultColor; + } + return colorTable[index]; + } + + 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/allenheath/xonek3/color/XoneIndexColor.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/XoneIndexColor.java new file mode 100644 index 00000000..971aec16 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/XoneIndexColor.java @@ -0,0 +1,90 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.color; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; + +public class XoneIndexColor extends InternalHardwareLightState { + + + public static final XoneIndexColor BLACK = new XoneIndexColor(0); + public static final XoneIndexColor RED = new XoneIndexColor(0x1); + public static final XoneIndexColor ORANGE = new XoneIndexColor(0x2); + public static final XoneIndexColor YELLOW = new XoneIndexColor(0x3); + public static final XoneIndexColor LIME = new XoneIndexColor(0x4); + public static final XoneIndexColor GREEN = new XoneIndexColor(0x5); + public static final XoneIndexColor TEAL = new XoneIndexColor(0x6); + public static final XoneIndexColor CYAN = new XoneIndexColor(0x7); + public static final XoneIndexColor AQUA = new XoneIndexColor(0x8); + public static final XoneIndexColor Blue = new XoneIndexColor(0x9); + public static final XoneIndexColor VIOLET = new XoneIndexColor(0xA); + public static final XoneIndexColor MAGENTA = new XoneIndexColor(0xB); + public static final XoneIndexColor LAVENDER = new XoneIndexColor(0xC); + public static final XoneIndexColor DARK_GREY = new XoneIndexColor(0xD); + public static final XoneIndexColor LIGHT_GREY = new XoneIndexColor(0xE); + public static final XoneIndexColor WHITE = new XoneIndexColor(0xF); + public static final XoneIndexColor BACKLIGHT = new XoneIndexColor(0x10); + + private final int colorIndex; + private final boolean on; + private final XoneIndexColor offState; + + private XoneIndexColor(final int colorIndex, final boolean isOn, final XoneIndexColor onState) { + this.colorIndex = colorIndex; + this.on = isOn; + this.offState = onState; + } + + public XoneIndexColor(final int colorIndex) { + this(colorIndex, true, new XoneIndexColor(colorIndex, false, null)); + } + + public int getColorIndex() { + return colorIndex; + } + + public boolean isOn() { + return on; + } + + public int stateValue() { + return on ? 0x7F : 0; + } + + public XoneIndexColor getOffState() { + return offState == null ? this : offState; + } + + @Override + public HardwareLightVisualState getVisualState() { + return null; + } + + public static XoneIndexColor forColor(final Color color) { + if (color == null || color.getAlpha() == 0) { + return XoneIndexColor.BLACK; + } + if (color.getRed255() == 0 && color.getGreen255() == 0 && color.getBlue255() == 0) { + return XoneIndexColor.BLACK; + } + return XoneIndexColor.RED; + } + + @Override + public boolean equals(final Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + + final XoneIndexColor that = (XoneIndexColor) o; + return colorIndex == that.colorIndex && on == that.on; + } + + @Override + public int hashCode() { + int result = colorIndex; + result = 31 * result + Boolean.hashCode(on); + return result; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/XoneRgbColor.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/XoneRgbColor.java new file mode 100644 index 00000000..4cdb506d --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/color/XoneRgbColor.java @@ -0,0 +1,212 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.color; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; + +public class XoneRgbColor extends InternalHardwareLightState { + public static final int FULL_BRIGHT = 0x30; + public static final int HALF_BRIGHT = 0x10; + public static final int DIM = 0x3; + + public static final XoneRgbColor OFF = new XoneRgbColor(0, 0, 0, 0); + public static final XoneRgbColor RED = new XoneRgbColor(127, 0, 0, FULL_BRIGHT); + public static final XoneRgbColor RED_OVER = new XoneRgbColor(127, 5, 0, FULL_BRIGHT); + public static final XoneRgbColor GREEN = new XoneRgbColor(0, 127, 0, FULL_BRIGHT); + public static final XoneRgbColor TEAL = new XoneRgbColor(0, 64, 64, FULL_BRIGHT); + public static final XoneRgbColor LIME = new XoneRgbColor(85, 127, 0, FULL_BRIGHT); + public static final XoneRgbColor YELLOW = new XoneRgbColor(127, 127, 0, FULL_BRIGHT); + public static final XoneRgbColor PURPLE = new XoneRgbColor(24, 2, 120, FULL_BRIGHT); + public static final XoneRgbColor MAGENTA = new XoneRgbColor(127, 1, 127, FULL_BRIGHT); + public static final XoneRgbColor PINK = new XoneRgbColor(127, 40, 80, FULL_BRIGHT); + public static final XoneRgbColor ORANGE = new XoneRgbColor(127, 20, 0, FULL_BRIGHT); + public static final XoneRgbColor WHITE = new XoneRgbColor(127, 127, 127, HALF_BRIGHT); + public static final XoneRgbColor GRAY = new XoneRgbColor(64, 64, 64, FULL_BRIGHT); + public static final XoneRgbColor DARK_GRAY = new XoneRgbColor(12, 12, 12, FULL_BRIGHT); + public static final XoneRgbColor CYAN = new XoneRgbColor(20, 127, 127, FULL_BRIGHT); + public static final XoneRgbColor BLUE = new XoneRgbColor(0, 20, 127, FULL_BRIGHT); + public static final XoneRgbColor AQUA = new XoneRgbColor(20, 110, 127, FULL_BRIGHT); + public static final XoneRgbColor ORANGE_REMOTES = new XoneRgbColor(127, 40, 2, FULL_BRIGHT); + + public static final XoneRgbColor RED_DIM = RED.asBright(DIM); + public static final XoneRgbColor GREEN_DIM = GREEN.asBright(DIM); + public static final XoneRgbColor YELLOW_DIM = YELLOW.asBright(DIM); + public static final XoneRgbColor ORANGE_DIM = ORANGE.asBright(DIM); + public static final XoneRgbColor BLUE_DIM = BLUE.asBright(DIM); + public static final XoneRgbColor MAGENTA_DIM = MAGENTA.asBright(DIM); + public static final XoneRgbColor WHITE_LO = WHITE.asBright(1); + public static final XoneRgbColor WHITE_DIM = WHITE.asBright(DIM); + public static final XoneRgbColor WHITE_HALF = WHITE.asBright(0x20); + + public static final XoneRgbColor RED_OVER_DIM = RED_OVER.asBright(DIM); + + private static final Map COLOR_MAP = new HashMap<>(); + + private final int red; + private final int green; + private final int blue; + private final int brightness; + + public static class BrightnessScale { + + private final List colorScales = new ArrayList<>(); + + public BrightnessScale(final XoneRgbColor baseColor, final int... brightnesses) { + for (int i = 0; i < brightnesses.length; i++) { + colorScales.add(baseColor.asBright(brightnesses[i])); + } + } + + public XoneRgbColor getColor(final int index) { + if (index < colorScales.size()) { + return colorScales.get(index); + } + return colorScales.get(colorScales.size() - 1); + } + } + + private static float inverseLogLike(float x, final float alpha) { + x = Math.max(0f, Math.min(1f, x)); + if (alpha == 0f) { + return x; + } + final double e = Math.exp(alpha); + return (float) ((Math.exp(alpha * x) - 1.0) / (e - 1.0)); + } + + public static XoneRgbColor getPaletteColor(final int paletteIndex) { + return switch (paletteIndex) { + case 0 -> OFF; + case 1 -> RED; + case 2 -> ORANGE; + case 3 -> YELLOW; + case 4 -> LIME; + case 5 -> GREEN; + case 6 -> TEAL; + case 7 -> CYAN; + case 8 -> AQUA; + case 9 -> BLUE; + case 10 -> PURPLE; + case 11 -> MAGENTA; + case 12 -> PINK; + case 13 -> DARK_GRAY; + case 14 -> GRAY; + case 15 -> WHITE; + default -> RED; + }; + } + + private static int flatten(final int color) { + return color < 4 ? 0 : color; + } + + public static XoneRgbColor of(final float r, final float g, final float b) { + return of(r, g, b, FULL_BRIGHT); + } + + public static XoneRgbColor of(final float r, final float g, final float b, final int brightness, + final XoneRgbColor zeroColor) { + final int red = Math.round(r * 127); + final int green = Math.round(g * 127); + final int blue = Math.round(b * 127); + if (red == 36 && blue == 36 && green == 36) { + return zeroColor; + } + final int code = red | (green << 7) | (blue << 14) | (brightness << 21); + return COLOR_MAP.computeIfAbsent(code, key -> createCorrected(r, g, b, brightness)); + } + + public BrightnessScale scaleOf(final int... brightness) { + return new BrightnessScale(this, brightness); + } + + public static XoneRgbColor of(final float r, final float g, final float b, final int brightness) { + final int red = Math.round(r * 127); + final int green = Math.round(g * 127); + final int blue = Math.round(b * 127); + final int code = red | (green << 7) | (blue << 14) | (brightness << 21); + return COLOR_MAP.computeIfAbsent(code, key -> createCorrected(r, g, b, brightness)); + } + + public static XoneRgbColor createCorrected(final float r, final float g, final float b, final int brightness) { + final float alpha = 3.5f; // >0 compresses low end; try 1.0 - 4.0 + final int red = flatten(Math.round(inverseLogLike(r, alpha) * 127f)); + final int green = flatten(Math.round(inverseLogLike(g, alpha) * 127f)); + final int blue = flatten(Math.round(inverseLogLike(b, alpha) * 127f)); + return new XoneRgbColor(red, green, blue, brightness); + } + + + public XoneRgbColor asBright(final int brightness) { + return new XoneRgbColor(red, green, blue, brightness); + } + + public XoneRgbColor bright(final int brightness) { + final int code = getBaseCode() | (brightness << 21); + return COLOR_MAP.computeIfAbsent(code, key -> new XoneRgbColor(red, green, blue, brightness)); + } + + private int getBaseCode() { + return red | (green << 7) | (blue << 14); + } + + private XoneRgbColor(final int red, final int green, final int blue, final int brightness) { + this.red = red; + this.green = green; + this.blue = blue; + this.brightness = brightness; + } + + public int getRed() { + return red; + } + + public int getGreen() { + return green; + } + + public int getBlue() { + return blue; + } + + public int getBrightness() { + return brightness; + } + + @Override + public HardwareLightVisualState getVisualState() { + return HardwareLightVisualState.createForColor(Color.fromRGB255(red * 2, green * 2, blue * 2)); + } + + @Override + public boolean equals(final Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + + final XoneRgbColor that = (XoneRgbColor) o; + return red == that.red && green == that.green && blue == that.blue && brightness == that.brightness; + } + + @Override + public int hashCode() { + int result = red; + result = 31 * result + green; + result = 31 * result + blue; + result = 31 * result + brightness; + return result; + } + + public static XoneRgbColor forColor(final Color color) { + if (color == null || color.getAlpha() == 0) { + return OFF; + } + return new XoneRgbColor(color.getRed255() / 2, color.getGreen255() / 2, color.getBlue255() / 2, FULL_BRIGHT); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneAssignButton.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneAssignButton.java new file mode 100644 index 00000000..13835b22 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneAssignButton.java @@ -0,0 +1,62 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.control; + +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneMidiDevice; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneIndexColor; +import com.bitwig.extensions.framework.values.MidiStatus; + +public class XoneAssignButton { + + private final HardwareButton hwButton; + private final MultiStateHardwareLight light; + private final XoneMidiDevice midiProcessor; + private final int midiNr; + private final int midiStatus; + private final int layerIndex; + private final int ledIndex; + + + public XoneAssignButton(final XoneMidiDevice midiProcessor, final HardwareSurface surface, final int layer, + final int ledIndex, final int midiNr) { + final String name = "GRID%d LAYER %d - %d".formatted(midiProcessor.getDeviceIndex(), layer + 2, ledIndex + 1); + hwButton = surface.createHardwareButton(name); + this.midiProcessor = midiProcessor; + this.midiNr = midiNr; + this.layerIndex = layer; + this.ledIndex = ledIndex; + final MidiIn midiIn = midiProcessor.getMidiIn(); + this.midiStatus = MidiStatus.NOTE_ON.getValue() | 0xE; + hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(0xE, midiNr)); + hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(0xE, midiNr)); + light = surface.createMultiStateHardwareLight("%s LIGHT".formatted(name)); + light.state().setValue(XoneIndexColor.BLACK); + light.setColorToStateFunction(XoneIndexColor::forColor); + hwButton.setBackgroundLight(light); + light.state().onUpdateHardware(this::handleState); + } + + public int getLayerIndex() { + return layerIndex; + } + + public int getLedIndex() { + return ledIndex; + } + + public int getMidiNr() { + return midiNr; + } + + private void handleState(final InternalHardwareLightState state) { + if (state instanceof final XoneIndexColor color) { + midiProcessor.updateAssignLed(this.midiStatus, midiNr, layerIndex, ledIndex, color != XoneIndexColor.BLACK); + } else { + midiProcessor.sendMidi(midiStatus, midiNr, 0); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneEncoder.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneEncoder.java new file mode 100644 index 00000000..70fba9a2 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneEncoder.java @@ -0,0 +1,34 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.control; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneMidiDevice; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.values.MidiStatus; + +public class XoneEncoder { + private final RelativeHardwareKnob hwEncoder; + private final XoneRgbButton pushButton; + + public XoneEncoder(final int index, final int ledIndex, final int channel, final int ccNr, final int buttonNoteNr, + final String name, final XoneMidiDevice midiProcessor, final HardwareSurface surface) { + this.hwEncoder = surface.createRelativeHardwareKnob("%s %d".formatted(name, index + 1)); + final MidiIn midiIn = midiProcessor.getMidiIn(); + + hwEncoder.setAdjustValueMatcher(midiIn.createRelative2sComplementCCValueMatcher(channel, ccNr, 40)); + hwEncoder.setStepSize(0.025); + pushButton = new XoneRgbButton( + index, ledIndex, "%s Button".formatted(name), MidiStatus.NOTE_ON, buttonNoteNr + index, channel, surface, + midiProcessor); + } + + public void bindEncoder(final Layer layer, final RelativeHardwarControlBindable bindable) { + layer.bind(hwEncoder, bindable); + } + + public XoneRgbButton getPushButton() { + return pushButton; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneRgbButton.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneRgbButton.java new file mode 100644 index 00000000..97230437 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/control/XoneRgbButton.java @@ -0,0 +1,126 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.control; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.bitwig.extension.controller.api.BooleanValue; +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.InternalHardwareLightState; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneMidiDevice; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneIndexColor; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneRgbColor; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.values.MidiStatus; + +public class XoneRgbButton { + + private final XoneMidiDevice midiProcessor; + private final int index; + private final int status; + private final MidiStatus midiStatus; + private final HardwareButton hwButton; + private final int midiNr; + private final MultiStateHardwareLight light; + private final int ledIndex; + private int lastColorIndex = -1; + + public XoneRgbButton(final int index, final int ledIndex, final String name, final MidiStatus midiStatus, + final int nr, final int channel, final HardwareSurface surface, final XoneMidiDevice midiProcessor) { + this.midiProcessor = midiProcessor; + this.index = index; + this.ledIndex = ledIndex; + this.midiNr = nr; + this.midiStatus = midiStatus; + this.status = midiStatus.getStatus(channel); + hwButton = surface.createHardwareButton("%s %d".formatted(name, index + 1)); + hwButton.isPressed().markInterested(); + final MidiIn midiIn = midiProcessor.getMidiIn(); + if (midiStatus == MidiStatus.NOTE_ON) { + hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(channel, nr)); + hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, nr)); + } else { + hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, nr, 0x7F)); + hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, nr, 0x0)); + } + light = surface.createMultiStateHardwareLight("%s LIGHT %d".formatted(name, index + 1)); + light.state().setValue(XoneIndexColor.BLACK); + light.setColorToStateFunction(XoneRgbColor::forColor); + hwButton.setBackgroundLight(light); + if (ledIndex != -1) { + light.state().onUpdateHardware(this::handleState); + } + } + + private void handleState(final InternalHardwareLightState state) { + if (state instanceof final XoneIndexColor color) { + updateColor(color); + } else if (state instanceof final XoneRgbColor color) { + updateColor(color); + } else { + midiProcessor.sendMidi(this.status, midiNr, 0); + } + } + + private void updateColor(final XoneRgbColor color) { + midiProcessor.updateLed(ledIndex, color.getRed(), color.getGreen(), color.getBlue(), color.getBrightness()); + } + + private void updateColor(final XoneIndexColor color) { + if (color.getColorIndex() != lastColorIndex) { + midiProcessor.sendMidi(this.status, midiNr, 0); + midiProcessor.configureLed(ledIndex, 0, color.getColorIndex(), this.midiStatus, this.midiNr); + midiProcessor.sendMidi(this.status, midiNr, color.stateValue()); + lastColorIndex = color.getColorIndex(); + } else { + midiProcessor.sendMidi(this.status, midiNr, color.stateValue()); + } + } + + 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 bindLight(final Layer layer, final Supplier supplier) { + layer.bindLightState(supplier, light); + } + + public void bindDisabled(final Layer layer) { + layer.bind(hwButton, hwButton.pressedAction(), () -> {}); + layer.bind(hwButton, hwButton.releasedAction(), () -> {}); + layer.bindLightState(() -> XoneRgbColor.OFF, light); + } + + public void bindLightPressed(final Layer layer, final InternalHardwareLightState holdState, + final InternalHardwareLightState releaseState) { + hwButton.isPressed().markInterested(); + layer.bindLightState(() -> hwButton.isPressed().get() ? holdState : releaseState, light); + } + + 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); + } + + public BooleanValue isPressed() { + return hwButton.isPressed(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/ClipSceneControl.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/ClipSceneControl.java new file mode 100644 index 00000000..89be5689 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/ClipSceneControl.java @@ -0,0 +1,263 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.layer; + +import java.util.List; + +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.Scene; +import com.bitwig.extension.controller.api.SceneBank; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.allenheath.xonek3.DeviceHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.ViewControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3GlobalStates; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneRgbColor; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneRgbButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Activate; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.values.BooleanValueObject; +import com.bitwig.extensions.framework.values.FocusMode; +import com.bitwig.extensions.framework.values.PanelLayout; + +@Component +public class ClipSceneControl { + private final XoneRgbColor[][] slotColors; + private final XoneRgbColor[] sceneColors = new XoneRgbColor[4]; + + private final Transport transport; + private final ViewControl viewControl; + private final BooleanValueObject shiftHeld; + private final XoneK3GlobalStates globalStates; + private FocusMode recordFocusMode; + private final Track rootTrack; + + protected SettableBooleanValue clipLauncherOverdub; + + private final Layer clipLaunchLayer; + private final Layer sceneLaunchLayer; + + private int sceneOffset; + + public ClipSceneControl(final ControllerHost host, final LayerCollection layers, final XoneHwElements hwElements, + final ViewControl viewControl, final Transport transport, final XoneK3GlobalStates globalStates, + final Application application) { + slotColors = new XoneRgbColor[4 * globalStates.getDeviceCount()][4]; + clipLaunchLayer = layers.getLayer(LayerId.CLIP_LAUNCHER); + sceneLaunchLayer = layers.getLayer(LayerId.SCENE_LAUNCHER); + final Layer layerChooserLayer = layers.getLayer(LayerId.LAYER_CHOOSER); + this.transport = transport; + rootTrack = host.getProject().getRootTrackGroup(); + this.shiftHeld = globalStates.getShiftHeld(); + this.viewControl = viewControl; + this.globalStates = globalStates; + application.panelLayout().addValueObserver(this::handlePanelLayoutChanged); + this.clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); + this.clipLauncherOverdub.markInterested(); + this.transport.isPlaying().markInterested(); + recordFocusMode = FocusMode.LAUNCHER; + + final SettableEnumValue recordButtonAssignment = host.getDocumentState().getEnumSetting( + "Record Button assignment", // + "Transport", new String[] {FocusMode.LAUNCHER.getDescriptor(), FocusMode.ARRANGER.getDescriptor()}, + recordFocusMode.getDescriptor()); + recordButtonAssignment.addValueObserver(value -> recordFocusMode = FocusMode.toMode(value)); + + + final TrackBank trackBank = viewControl.getTrackBank(); + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(i); + for (int j = 0; j < trackBank.sceneBank().getSizeOfBank(); j++) { + final int sceneIndex = j; + slotColors[trackIndex][sceneIndex] = XoneRgbColor.OFF; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(j); + prepareSlot(slot, trackIndex, sceneIndex); + final XoneRgbButton button = hwElements.getGridButton(trackIndex, sceneIndex); + button.bindLight(clipLaunchLayer, () -> determineColor(track, slot, trackIndex, sceneIndex)); + button.bindPressed(clipLaunchLayer, () -> launchSlot(slot, trackIndex, sceneIndex)); + button.bindRelease(clipLaunchLayer, () -> releaseSlot(slot, trackIndex, sceneIndex)); + } + } + bindSceneLaunching(hwElements, trackBank); + bindTransportOnGrid(layerChooserLayer, hwElements.getDeviceElements(0).getKnobButtons(), transport); + } + + private void bindSceneLaunching(final XoneHwElements hwElements, final TrackBank trackBank) { + final SceneBank sceneBank = trackBank.sceneBank(); + sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); + final DeviceHwElements deviceElements = hwElements.getDeviceElements(0); + for (int i = 0; i < 4; i++) { + final int sceneIndex = i; + final Scene scene = sceneBank.getScene(i); + scene.color().addValueObserver( + (r, g, b) -> sceneColors[sceneIndex] = XoneRgbColor.of(r, g, b, 0x20, XoneRgbColor.GREEN_DIM)); + scene.clipCount().markInterested(); + scene.exists().markInterested(); + final XoneRgbButton sceneButton = deviceElements.getGridButtons().get(i * 4); + sceneButton.bindLight(sceneLaunchLayer, () -> getSceneState(sceneIndex, scene)); + sceneButton.bindPressed(sceneLaunchLayer, () -> launchScene(scene)); + sceneButton.bindRelease(sceneLaunchLayer, () -> releaseScene(scene)); + for (int j = 1; j < globalStates.getDeviceCount() * 4; j++) { + final XoneRgbButton button = hwElements.getGridButton(j, i); + if (j != 3 || i != 3) { + button.bindDisabled(sceneLaunchLayer); + } + } + } + + + final Track rootTrack = viewControl.getRootTrack(); + final XoneRgbButton stopAllButton = deviceElements.getGridButtons().get(15); + stopAllButton.bindPressed(sceneLaunchLayer, () -> rootTrack.stop()); + stopAllButton.bindLightPressed(sceneLaunchLayer, XoneRgbColor.WHITE, XoneRgbColor.WHITE_DIM); + } + + + private void bindTransportOnGrid(final Layer layer, final List buttons, final Transport transport) { + final XoneRgbButton playButton = buttons.get(11); + transport.isPlaying().markInterested(); + playButton.bindPressed(layer, () -> transport.play()); + playButton.bindLight(layer, () -> transport.isPlaying().get() ? XoneRgbColor.GREEN : XoneRgbColor.GREEN_DIM); + + final XoneRgbButton stopButton = buttons.get(10); + stopButton.bindPressed(layer, () -> transport.stop()); + stopButton.bindLight(layer, () -> transport.isPlaying().get() ? XoneRgbColor.WHITE : XoneRgbColor.WHITE_DIM); + final XoneRgbButton recButton = buttons.get(9); + transport.isArrangerRecordEnabled().markInterested(); + transport.isArrangerOverdubEnabled().markInterested(); + recButton.bindPressed(layer, () -> transport.record()); + recButton.bindLight( + layer, () -> transport.isArrangerRecordEnabled().get() ? XoneRgbColor.RED : XoneRgbColor.RED_DIM); + + final XoneRgbButton overdubArrangeButton = buttons.get(8); + transport.isArrangerAutomationWriteEnabled().markInterested(); + overdubArrangeButton.bindPressed(layer, () -> toggleOverdub(transport)); + overdubArrangeButton.bindLight(layer, () -> overdubColorState(transport)); + } + + private XoneRgbColor overdubColorState(final Transport transport) { + if (recordFocusMode == FocusMode.ARRANGER) { + return transport.isArrangerOverdubEnabled().get() ? XoneRgbColor.RED : XoneRgbColor.RED_DIM; + } else { + return transport.isClipLauncherOverdubEnabled().get() ? XoneRgbColor.RED : XoneRgbColor.RED_DIM; + } + } + + private void toggleOverdub(final Transport transport) { + if (recordFocusMode == FocusMode.ARRANGER) { + transport.isArrangerOverdubEnabled().toggle(); + } else { + transport.isClipLauncherOverdubEnabled().toggle(); + } + } + + private void launchScene(final Scene scene) { + if (shiftHeld.get()) { + scene.launchAlt(); + } else { + scene.launch(); + } + } + + private void releaseScene(final Scene scene) { + if (shiftHeld.get()) { + scene.launchReleaseAlt(); + } else { + scene.launchRelease(); + } + } + + private XoneRgbColor getSceneState(final int sceneIndex, final Scene scene) { + if (!scene.exists().get()) { + return XoneRgbColor.OFF; + } + if (viewControl.hasQueuedClips(sceneOffset + sceneIndex)) { + return globalStates.blinkFast(sceneColors[sceneIndex]); + } + if (viewControl.hasPlayingClips(sceneOffset + sceneIndex)) { + return globalStates.pulse(sceneColors[sceneIndex]); + } + + return sceneColors[sceneIndex]; + } + + @Activate + public void activate() { + clipLaunchLayer.setIsActive(true); + sceneLaunchLayer.setIsActive(false); + } + + private void prepareSlot(final ClipLauncherSlot slot, final int trackIndex, final int sceneIndex) { + slot.hasContent().markInterested(); + slot.isPlaying().markInterested(); + slot.isStopQueued().markInterested(); + slot.isRecordingQueued().markInterested(); + slot.isRecording().markInterested(); + slot.isPlaybackQueued().markInterested(); + slot.color().addValueObserver((r, g, b) -> slotColors[trackIndex][sceneIndex] = XoneRgbColor.of(r, g, b)); + } + + private void launchSlot(final ClipLauncherSlot slot, final int trackIndex, final int sceneIndex) { + if (shiftHeld.get()) { + slot.launchAlt(); + } else { + slot.launch(); + } + } + + private void releaseSlot(final ClipLauncherSlot slot, final int trackIndex, final int sceneIndex) { + if (shiftHeld.get()) { + slot.launchReleaseAlt(); + } else { + slot.launchRelease(); + } + } + + private XoneRgbColor determineColor(final Track track, final ClipLauncherSlot slot, final int trackIndex, + final int sceneIndex) { + final XoneRgbColor color = slotColors[trackIndex][sceneIndex]; + if (slot.hasContent().get()) { + if (slot.isRecordingQueued().get()) { + return globalStates.blinkMid(XoneRgbColor.RED); + } else if (slot.isRecording().get()) { + return globalStates.pulse(XoneRgbColor.RED); + } else if (slot.isPlaybackQueued().get()) { + return globalStates.blinkFast(color); + } else if (slot.isStopQueued().get()) { + return globalStates.blinkFast(XoneRgbColor.GREEN); + } else if (slot.isPlaying().get() && track.isQueuedForStop().get()) { + return globalStates.pulse(XoneRgbColor.GREEN); + } else if (slot.isPlaying().get()) { + if (clipLauncherOverdub.get() && track.arm().get()) { + return globalStates.pulse(XoneRgbColor.RED); + } else { + if (transport.isPlaying().get()) { + return globalStates.pulse(XoneRgbColor.GREEN); + } + return XoneRgbColor.GREEN; + } + } + return color; + } + if (slot.isRecordingQueued().get()) { + return globalStates.blinkFast(XoneRgbColor.RED); + } + return XoneRgbColor.OFF; + } + + private void handlePanelLayoutChanged(final String value) { + final PanelLayout panelLayout; + if (value.equals("MIX")) { + panelLayout = PanelLayout.LAUNCHER; + } else { + panelLayout = PanelLayout.ARRANGER; + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/EqControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/EqControlLayer.java new file mode 100644 index 00000000..d750cae7 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/EqControlLayer.java @@ -0,0 +1,114 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.layer; + +import java.util.List; + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.controllers.allenheath.xonek3.DeviceHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.TrackSpecControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.ViewControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3GlobalStates; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneRgbColor; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneRgbButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public class EqControlLayer extends Layer { + + private final XoneK3GlobalStates globalStates; + private final Layer shiftLayer; + + public EqControlLayer(final Layers layers, final ViewControl viewControl, final XoneHwElements hwElements, + final XoneK3GlobalStates globalStates) { + super(layers, "DJ_EQ"); + this.globalStates = globalStates; + shiftLayer = new Layer(layers, "EQ_SHIFT_LAYER"); + this.globalStates.getShiftHeld().addValueObserver(active -> { + if (isActive()) { + shiftLayer.setIsActive(active); + } + }); + + final List specControls = viewControl.getSpecControls(); + for (int i = 0; i < specControls.size(); i++) { + final TrackSpecControl specControl = specControls.get(i); + bind(i, specControl, hwElements.getDeviceElements(i / 4)); + } + } + + private void bind(final int index, final TrackSpecControl control, final DeviceHwElements hwElements) { + final int controlIndex = index % 4; + final XoneRgbButton button1 = hwElements.getKnobButtons().get(controlIndex); + button1.bindLight( + this, () -> killButtonColor( + control, control.getHiKill(), XoneRgbColor.WHITE, XoneRgbColor.BLUE_DIM, + XoneRgbColor.BLUE)); + button1.bindPressed( + this, () -> { + if (control.eqExists()) { + handleToggle(control.getHiKill()); + } else { + control.insertEq(); + } + }); + + button1.bindLight( + shiftLayer, () -> !control.eqExists() + ? XoneRgbColor.WHITE + : (control.isDjEqActive() ? XoneRgbColor.ORANGE : XoneRgbColor.ORANGE_DIM)); + button1.bindPressed( + shiftLayer, () -> { + if (control.eqExists()) { + control.toggleDjEqActive(); + } + }); + + final XoneRgbButton button2 = hwElements.getKnobButtons().get(4 + controlIndex); + button2.bindLight( + this, () -> killButtonColor( + control, control.getMidKill(), XoneRgbColor.OFF, XoneRgbColor.YELLOW_DIM, + XoneRgbColor.YELLOW)); + button2.bindPressed(this, () -> handleToggle(control.getMidKill())); + + final XoneRgbButton button3 = hwElements.getKnobButtons().get(8 + controlIndex); + button3.bindLight( + this, () -> killButtonColor( + control, control.getLowKill(), XoneRgbColor.OFF, XoneRgbColor.RED_DIM, + XoneRgbColor.RED)); + button3.bindPressed(this, () -> handleToggle(control.getLowKill())); + + this.bind(hwElements.getKnobs().get(controlIndex), control.getHiGain()); + this.bind(hwElements.getKnobs().get(4 + controlIndex), control.getMidGain()); + this.bind(hwElements.getKnobs().get(8 + controlIndex), control.getLowGain()); + + shiftLayer.bind(hwElements.getKnobs().get(controlIndex), control.getHiFreq()); + shiftLayer.bind(hwElements.getKnobs().get(8 + controlIndex), control.getLowFreq()); + } + + private XoneRgbColor killButtonColor(final TrackSpecControl control, final Parameter toggleParam, + final XoneRgbColor nonColor, final XoneRgbColor offColor, final XoneRgbColor onColor) { + if (!control.eqExists()) { + return nonColor; + } + return toggleParam.value().get() > 0 ? offColor : onColor; + } + + private void handleToggle(final Parameter toggleParam) { + if (toggleParam.get() == 0) { + toggleParam.value().set(1.0); + } else { + toggleParam.value().set(0); + } + } + + @Override + protected void onActivate() { + super.onActivate(); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + shiftLayer.setIsActive(false); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/GridMode.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/GridMode.java new file mode 100644 index 00000000..2090abf8 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/GridMode.java @@ -0,0 +1,16 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.layer; + +public enum GridMode { + CLIP(LayerId.CLIP_LAUNCHER), // + SCENE(LayerId.SCENE_LAUNCHER), // + TRANSPORT(LayerId.GRID_TRANSPORT); + private LayerId layerId; + + GridMode(final LayerId layerId) { + this.layerId = layerId; + } + + public LayerId getLayerId() { + return layerId; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/LayerCollection.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/LayerCollection.java new file mode 100644 index 00000000..7b21e555 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/LayerCollection.java @@ -0,0 +1,63 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.layer; + +import java.util.HashMap; +import java.util.Map; + +import com.bitwig.extensions.controllers.allenheath.xonek3.DeviceHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.ViewControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3GlobalStates; +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; + +@Component +public class LayerCollection { + + private final Map layerMap = new HashMap(); + + private final LayerId[] initLayers = {LayerId.MAIN, LayerId.MIXER, LayerId.CLIP_LAUNCHER}; + + public LayerCollection(final Layers layers, final ViewControl viewControl, final XoneHwElements hwElements, + final XoneK3GlobalStates globalStates) { + for (final LayerId layerId : LayerId.values()) { + switch (layerId) { + case REMOTES -> layerMap.put(layerId, new RemotesLayer(layers, viewControl, hwElements, globalStates)); + case DJ_EQ -> layerMap.put(layerId, new EqControlLayer(layers, viewControl, hwElements, globalStates)); + case MIXER -> layerMap.put(layerId, new MixerLayer(layers, viewControl, hwElements, globalStates)); + case IND_REMOTES -> + layerMap.put(layerId, new SingleRemotesControlLayer(layers, viewControl, hwElements, globalStates)); + default -> layerMap.put(layerId, new Layer(layers, layerId.toString())); + } + } + + // TODO Consider something when 2 devices in play + final DeviceHwElements deviceHwElements = hwElements.getDeviceElements(0); + RemotesLayer.bindStandardRemoteControl( + layerMap.get(LayerId.TRACK_REMOTES), deviceHwElements, + viewControl.getTrackRemotes(), null); + RemotesLayer.bindStandardRemoteControl( + layerMap.get(LayerId.PROJECT_REMOTES), deviceHwElements, + viewControl.getProjectRemotes(), null); + hwElements.disableKnobButtonSectionRightSide(layerMap.get(LayerId.TRACK_REMOTES)); + hwElements.disableKnobButtonSectionRightSide(layerMap.get(LayerId.PROJECT_REMOTES)); + + } + + @Activate + public void init() { + for (final LayerId id : initLayers) { + layerMap.get(id).setIsActive(true); + } + } + + public Layer getLayer(final LayerId id) { + return layerMap.get(id); + } + + + public void setActive(final LayerId layerId, final boolean active) { + layerMap.get(layerId).setIsActive(active); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/LayerId.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/LayerId.java new file mode 100644 index 00000000..82964660 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/LayerId.java @@ -0,0 +1,16 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.layer; + +public enum LayerId { + MAIN, + MIXER, + REMOTES, + DJ_EQ, + IND_REMOTES, + CLIP_LAUNCHER, + SCENE_LAUNCHER, + GRID_TRANSPORT, + TRACK_REMOTES, + PROJECT_REMOTES, + LAYER_CHOOSER, + GRID_LAYER_CHOOSER +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/MixerLayer.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/MixerLayer.java new file mode 100644 index 00000000..b7ebd120 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/MixerLayer.java @@ -0,0 +1,204 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.layer; + +import java.util.List; + +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.allenheath.xonek3.DeviceHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.ViewControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3GlobalStates; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneRgbColor; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneEncoder; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneRgbButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public class MixerLayer extends Layer { + private final TrackState[] trackState; + private static final int[] BRIGHTNESS_GRADS = {1, 3, 4, 8, 20}; + private static final XoneRgbColor.BrightnessScale WHITE_SCALE = XoneRgbColor.WHITE.scaleOf(BRIGHTNESS_GRADS); + private final Layer shiftLayer; + private final XoneK3GlobalStates globalStates; + + private static class TrackState { + private XoneRgbColor color; + private XoneRgbColor halfColor; + private XoneRgbColor dimColor; + private boolean isPlaying = false; + private boolean isInstrument = false; + private int vuState = 0; + private boolean isSelected = false; + private final Track track; + + public TrackState(final Track track) { + track.addIsSelectedInMixerObserver(selected -> this.isSelected = selected); + this.track = track; + track.color().addValueObserver((r, g, b) -> { + color = XoneRgbColor.of(r, g, b, XoneRgbColor.FULL_BRIGHT); + halfColor = color.bright(XoneRgbColor.HALF_BRIGHT); + dimColor = color.bright(XoneRgbColor.DIM); + }); + track.playingNotes().addValueObserver(playingNotes -> { + isPlaying = playingNotes.length > 0; + }); + track.trackType().addValueObserver(trackType -> { + isInstrument = "Instrument".equals(trackType); + }); + track.addVuMeterObserver(5, -1, true, vu -> this.vuState = vu); + } + + public boolean exists() { + return track.exists().get(); + } + + public XoneRgbColor activityState() { + if (isSelected) { + if (isInstrument) { + return isPlaying ? XoneRgbColor.WHITE : XoneRgbColor.WHITE_HALF; + } + return XoneRgbColor.WHITE; + } + if (isInstrument) { + return isPlaying ? color : halfColor; + } + return switch (vuState) { + case 0 -> dimColor; + case 1, 2 -> halfColor; + case 3 -> color; + default -> XoneRgbColor.RED; + }; + } + + public XoneRgbColor selectColor() { + if (isSelected) { + return XoneRgbColor.WHITE; + } + return halfColor; + } + } + + public MixerLayer(final Layers layers, final ViewControl viewControl, final XoneHwElements hwElements, + final XoneK3GlobalStates globalStates) { + super(layers, "MIXER"); + this.shiftLayer = new Layer(layers, "MIXER_SHIFT_LAYER"); + this.globalStates = globalStates; + globalStates.getShiftHeld().addValueObserver(this::handleShift); + final TrackBank trackBank = viewControl.getTrackBank(); + this.trackState = new TrackState[trackBank.getSizeOfBank()]; + trackBank.setShouldShowClipLauncherFeedback(true); + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final int index = i; + final Track track = trackBank.getItemAt(i); + bindTrack(track, index, hwElements.getDeviceElements(index / 4)); + } + } + + private void handleShift(final boolean shiftHeld) { + if (isActive()) { + shiftLayer.setIsActive(shiftHeld); + } + } + + private void bindTrack(final Track track, final int index, final DeviceHwElements hwElements) { + final List sliders = hwElements.getSliders(); + final List knobs = hwElements.getKnobs(); + final List knobButtons = hwElements.getKnobButtons(); + final List encoders = hwElements.getEncoders(); + final int controlIndex = index % 4; + + trackState[index] = new TrackState(track); + this.bind(knobs.get(controlIndex), track.sendBank().getItemAt(0).value()); + this.bind(knobs.get(controlIndex + 4), track.sendBank().getItemAt(1).value()); + this.bind(knobs.get(controlIndex + 8), track.pan().value()); + this.bind(sliders.get(controlIndex), track.volume().value()); + for (int i = 0; i < 3; i++) { + shiftLayer.bind(knobs.get(controlIndex + i * 4), track.sendBank().getItemAt(i + 2).value()); + } + + final XoneRgbButton muteButton = knobButtons.get(controlIndex + 8); + final XoneRgbButton soloButton = knobButtons.get(controlIndex + 4); + final XoneRgbButton armButton = knobButtons.get(controlIndex); + armButton.bindLight(this, () -> armColorState(track)); + armButton.bindPressed(this, () -> track.arm().toggle()); + + soloButton.bindLight(this, () -> soloColorState(track)); + soloButton.bindPressed(this, () -> track.solo().toggleUsingPreferences(true)); + + muteButton.bindLight(this, () -> muteColorState(track)); + muteButton.bindPressed(this, () -> track.mute().toggle()); + + // TODO GROUP TRACKs + Bink things + armButton.bindLight(shiftLayer, () -> groupColorState(track)); + armButton.bindPressed(shiftLayer, () -> handleGroupTrack(track)); + + final TrackState trackState = this.trackState[index]; + soloButton.bindLight(shiftLayer, () -> stopColorState(trackState)); + soloButton.bindPressed(shiftLayer, () -> track.stop()); + + muteButton.bindLight(shiftLayer, () -> selectColorState(trackState)); + muteButton.bindPressed(shiftLayer, () -> track.selectInMixer()); + + final XoneRgbButton encoderButton = encoders.get(controlIndex).getPushButton(); + encoderButton.bindLight(this, trackState::activityState); + } + + private void handleGroupTrack(final Track track) { + if (track.isGroup().get()) { + track.isGroupExpanded().toggle(); + } + } + + private InternalHardwareLightState groupColorState(final Track track) { + if (!track.exists().get()) { + return XoneRgbColor.OFF; + } + if (track.isGroupExpanded().get()) { + return XoneRgbColor.AQUA; + } + if (track.isGroup().get()) { + return XoneRgbColor.WHITE; + } + return XoneRgbColor.WHITE_LO; + } + + private static XoneRgbColor armColorState(final Track track) { + return track.exists().get() ? (track.arm().get() ? XoneRgbColor.RED : XoneRgbColor.RED_DIM) : XoneRgbColor.OFF; + } + + private static XoneRgbColor soloColorState(final Track track) { + return track.exists().get() + ? (track.solo().get() ? XoneRgbColor.YELLOW : XoneRgbColor.YELLOW_DIM) + : XoneRgbColor.OFF; + } + + private static XoneRgbColor muteColorState(final Track track) { + return track.exists().get() + ? (track.mute().get() ? XoneRgbColor.ORANGE : XoneRgbColor.ORANGE_DIM) + : XoneRgbColor.OFF; + } + + private XoneRgbColor stopColorState(final TrackState track) { + if (!track.exists()) { + return XoneRgbColor.OFF; + } + if (track.track.isQueuedForStop().get()) { + return globalStates.blinkMid(XoneRgbColor.BLUE); + } + if (track.track.isStopped().get()) { + return XoneRgbColor.BLUE_DIM; + } + return XoneRgbColor.BLUE; + } + + private static XoneRgbColor selectColorState(final TrackState track) { + if (track.exists()) { + return track.selectColor(); + } + return XoneRgbColor.OFF; + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/RemotesLayer.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/RemotesLayer.java new file mode 100644 index 00000000..c58897ca --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/RemotesLayer.java @@ -0,0 +1,266 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.layer; + +import java.util.List; + +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.CursorDevice; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.RemoteControl; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extensions.controllers.allenheath.xonek3.DeviceHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.TrackSpecControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.ViewControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3ControllerExtension; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3GlobalStates; +import com.bitwig.extensions.controllers.allenheath.xonek3.color.XoneRgbColor; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneRgbButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public class RemotesLayer extends Layer { + + private final Layer shiftLayer; + public static final int[] BRIGHT = {1, 0xF, 0x30}; + public static final XoneRgbColor.BrightnessScale[] PARAM_COLORS = { + XoneRgbColor.RED.scaleOf(BRIGHT), XoneRgbColor.ORANGE.scaleOf(BRIGHT), XoneRgbColor.YELLOW.scaleOf(BRIGHT), + XoneRgbColor.GREEN.scaleOf(BRIGHT), XoneRgbColor.CYAN.scaleOf(BRIGHT), XoneRgbColor.BLUE.scaleOf(BRIGHT), + XoneRgbColor.PURPLE.scaleOf(BRIGHT), XoneRgbColor.MAGENTA.scaleOf(BRIGHT) + }; + private static final XoneRgbColor.BrightnessScale WHITE = XoneRgbColor.WHITE.scaleOf(BRIGHT); + private static final XoneRgbColor.BrightnessScale AQUA = XoneRgbColor.AQUA.scaleOf(BRIGHT); + private static final XoneRgbColor.BrightnessScale DEVICE = XoneRgbColor.YELLOW.scaleOf(BRIGHT); + + private final XoneK3GlobalStates globalStates; + + private static class DeviceState { + private boolean exists; + private XoneRgbColor stdColor; + private XoneRgbColor selectColor; + private XoneRgbColor disabledColor; + private boolean enabled; + private boolean selected; + private final Device device; + + public DeviceState(final Device device, final CursorDevice cursorDevice) { + this.device = device; + device.exists().addValueObserver(exists -> this.exists = exists); + device.deviceType().addValueObserver(this::handleDeviceType); + device.isEnabled().addValueObserver(enabled -> this.enabled = enabled); + final BooleanValue onCursor = cursorDevice.createEqualsValue(device); + onCursor.addValueObserver(select -> this.selected = select); + applyColor(XoneRgbColor.WHITE); + } + + private void applyColor(final XoneRgbColor color) { + this.stdColor = color; + this.disabledColor = color.bright(XoneRgbColor.DIM); + this.selectColor = color.bright(XoneRgbColor.FULL_BRIGHT); + } + + private void handleDeviceType(final String type) { + if ("instrument".equals(type)) { + applyColor(XoneRgbColor.YELLOW.bright(XoneRgbColor.HALF_BRIGHT)); + } else if ("audio_to_audio".equals(type)) { + applyColor(XoneRgbColor.ORANGE.bright(XoneRgbColor.HALF_BRIGHT)); + } else { + applyColor(XoneRgbColor.BLUE.bright(XoneRgbColor.HALF_BRIGHT)); + } + } + + public InternalHardwareLightState getColor() { + if (exists) { + if (selected) { + return selectColor; + } + if (!enabled) { + return disabledColor; + } + return stdColor; + } + return XoneRgbColor.OFF; + } + + public void toggleEnable() { + device.isEnabled().toggle(); + } + } + + public RemotesLayer(final Layers layers, final ViewControl viewControl, final XoneHwElements hwElements, + final XoneK3GlobalStates globalStates) { + super(layers, "REMOTES"); + this.globalStates = globalStates; + shiftLayer = new Layer(layers, "REMOTES_SHIFT"); + final CursorRemoteControlsPage remotes = viewControl.getDeviceRemotePages(); + final DeviceHwElements deviceElements = hwElements.getDeviceElements(0); + hwElements.disableKnobButtonSectionRightSide(this); + + bindStandardRemoteControl(this, deviceElements, remotes, viewControl.getCursorDevice()); + globalStates.getShiftHeld().addValueObserver(shift -> { + if (isActive()) { + shiftLayer.setIsActive(shift); + } + }); + final PinnableCursorDevice cursorDevice = viewControl.getCursorDevice(); + + final List buttons = deviceElements.getKnobButtons(); + final XoneRgbButton leftBottomButton = buttons.get(8); + final XoneRgbButton rightBottomButton = buttons.get(9); + cursorDevice.isEnabled().markInterested(); + cursorDevice.exists().markInterested(); + + leftBottomButton.bindLight(shiftLayer, () -> deviceEnabledState(cursorDevice)); + leftBottomButton.bindPressed(shiftLayer, () -> cursorDevice.isEnabled().toggle()); + rightBottomButton.bindLight(shiftLayer, () -> XoneRgbColor.OFF); + rightBottomButton.bindPressed(shiftLayer, () -> {}); + } + + private static XoneRgbColor deviceEnabledState(final PinnableCursorDevice cursorDevice) { + if (cursorDevice.exists().get()) { + return cursorDevice.isEnabled().get() ? DEVICE.getColor(2) : DEVICE.getColor(0); + } + return XoneRgbColor.GRAY.bright(01); + } + + public static void bindStandardRemoteControl(final Layer layer, final DeviceHwElements hwElements, + final CursorRemoteControlsPage remotes, final PinnableCursorDevice cursorDevice) { + final List knobs = hwElements.getKnobs(); + final List buttons = hwElements.getKnobButtons(); + remotes.pageCount().markInterested(); + remotes.selectedPageIndex().markInterested(); + for (int i = 0; i < 8; i++) { + final int index = i; + final XoneRgbButton button = buttons.get(i); + final AbsoluteHardwareControl knob = knobs.get(i); + final RemoteControl param = remotes.getParameter(i); + + param.exists().markInterested(); + param.value().markInterested(); + param.discreteValueCount().markInterested(); + button.bindLight(layer, () -> getParamState(index, param)); + button.bindPressed(layer, () -> toggleParameter(param)); + layer.bind(knob, param.value()); + } + for (int i = 0; i < 4; i++) { + final int index = i + 8; + final AbsoluteHardwareControl knob = knobs.get(index); + layer.bind(knob, v -> {}); + } + final XoneRgbButton devicePreviousButton = buttons.get(8); + final XoneRgbButton deviceNextButton = buttons.get(9); + if (cursorDevice != null) { + cursorDevice.hasPrevious().markInterested(); + cursorDevice.hasNext().markInterested(); + cursorDevice.exists().markInterested(); + devicePreviousButton.bindLight(layer, () -> deviceNavigateLeftColor(cursorDevice)); + devicePreviousButton.bindPressed(layer, () -> cursorDevice.selectPrevious()); + deviceNextButton.bindLight(layer, () -> deviceNavigateRightColor(cursorDevice)); + deviceNextButton.bindPressed(layer, () -> cursorDevice.selectNext()); + } else { + devicePreviousButton.bindLight(layer, () -> XoneRgbColor.OFF); + devicePreviousButton.bindPressed(layer, () -> {}); + deviceNextButton.bindLight(layer, () -> XoneRgbColor.OFF); + deviceNextButton.bindPressed(layer, () -> {}); + } + final XoneRgbButton remotesPreviousButton = buttons.get(10); + final XoneRgbButton remotesNextButton = buttons.get(11); + remotes.selectedPageIndex().markInterested(); + remotes.pageCount().markInterested(); + remotesPreviousButton.bindLight(layer, () -> remotesNavigateLeftColor(remotes)); + remotesPreviousButton.bindPressed(layer, () -> remotes.selectedPageIndex().inc(-1)); + remotesNextButton.bindLight(layer, () -> remotesNavigateRightColor(remotes)); + remotesNextButton.bindPressed(layer, () -> remotes.selectedPageIndex().inc(1)); + } + + private static XoneRgbColor deviceNavigateLeftColor(final PinnableCursorDevice device) { + if (device.exists().get()) { + return device.hasPrevious().get() ? WHITE.getColor(2) : WHITE.getColor(0); + } + return XoneRgbColor.GRAY.bright(1); + } + + + private static XoneRgbColor deviceNavigateRightColor(final PinnableCursorDevice device) { + if (device.exists().get()) { + return device.hasNext().get() ? WHITE.getColor(2) : WHITE.getColor(0); + } + return XoneRgbColor.GRAY.bright(1); + } + + private static XoneRgbColor remotesNavigateLeftColor(final CursorRemoteControlsPage remotes) { + if (remotes.pageCount().get() == 0) { + return AQUA.getColor(0); + } + return remotes.selectedPageIndex().get() > 0 ? AQUA.getColor(2) : AQUA.getColor(1); + } + + private static XoneRgbColor remotesNavigateRightColor(final CursorRemoteControlsPage remotes) { + if (remotes.pageCount().get() == 0) { + return AQUA.getColor(0); + } + return remotes.selectedPageIndex().get() + 1 < remotes.pageCount().get() ? AQUA.getColor(2) : AQUA.getColor(1); + } + + public static void toggleParameter(final RemoteControl param) { + final SettableRangedValue paramValue = param.value(); + final double value = paramValue.get(); + if (param.discreteValueCount().get() == -1) { + XoneK3ControllerExtension.println(" > %f", value); + if (value > 0.5) { + paramValue.setImmediately(0); + } else { + paramValue.setImmediately(1.0); + } + } else { + final int intVal = (int) Math.round(paramValue.get() * param.discreteValueCount().get()) + 1; + + if (intVal <= param.discreteValueCount().get()) { + paramValue.set(intVal, param.discreteValueCount().get()); + } else { + paramValue.set(0); + } + } + } + + public static XoneRgbColor getParamState(final int index, final RemoteControl param) { + if (param.exists().get()) { + if (param.value().get() > 0) { + return PARAM_COLORS[index].getColor(2); + } else { + return PARAM_COLORS[index].getColor(1); + } + } + return PARAM_COLORS[index].getColor(0); + } + + public static XoneRgbColor getParamState(final int index, final RemoteControl param, + final TrackSpecControl trackSpecControl) { + if (param.exists().get()) { + if (param.value().get() > 0) { + return PARAM_COLORS[index].getColor(2); + } else { + return PARAM_COLORS[index].getColor(1); + } + } + if (trackSpecControl.isTrackExists()) { + return PARAM_COLORS[index].getColor(0); + } + return XoneRgbColor.OFF; + } + + + @Override + protected void onDeactivate() { + super.onDeactivate(); + this.shiftLayer.setIsActive(false); + } + + @Override + protected void onActivate() { + super.onActivate(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/SingleRemotesControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/SingleRemotesControlLayer.java new file mode 100644 index 00000000..72b691e8 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/allenheath/xonek3/layer/SingleRemotesControlLayer.java @@ -0,0 +1,79 @@ +package com.bitwig.extensions.controllers.allenheath.xonek3.layer; + +import java.util.List; + +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.RemoteControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.DeviceHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.TrackSpecControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.ViewControl; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneHwElements; +import com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3GlobalStates; +import com.bitwig.extensions.controllers.allenheath.xonek3.control.XoneRgbButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public class SingleRemotesControlLayer extends Layer { + + private final XoneK3GlobalStates globalStates; + private final Layer shiftLayer; + + public SingleRemotesControlLayer(final Layers layers, final ViewControl viewControl, + final XoneHwElements hwElements, final XoneK3GlobalStates globalStates) { + super(layers, "SINGLE_REMOTES"); + this.globalStates = globalStates; + shiftLayer = new Layer(layers, "SINGLE_REMOTES_SHIFT_LAYER"); + this.globalStates.getShiftHeld().addValueObserver(active -> { + if (isActive()) { + shiftLayer.setIsActive(active); + } + }); + final int trackCount = globalStates.getDeviceCount() * 4; + final List specControls = viewControl.getSpecControls(); + for (int i = 0; i < trackCount; i++) { + final TrackSpecControl specControl = specControls.get(i); + bind(i % 4, specControl, hwElements.getDeviceElements(i / 4)); + } + } + + private void bind(final int index, final TrackSpecControl control, final DeviceHwElements hwElements) { + final CursorRemoteControlsPage remotes = control.getTrackRemotes(); + final List knobs = hwElements.getKnobs(); + final List buttons = hwElements.getKnobButtons(); + + this.bind(knobs.get(index), remotes.getParameter(0)); + this.bind(knobs.get(4 + index), remotes.getParameter(1)); + this.bind(knobs.get(8 + index), remotes.getParameter(2)); + shiftLayer.bind(knobs.get(index), remotes.getParameter(3)); + shiftLayer.bind(knobs.get(index + 4), remotes.getParameter(4)); + shiftLayer.bind(knobs.get(index + 8), remotes.getParameter(5)); + for (int rowIndex = 0; rowIndex < 3; rowIndex++) { + bindParamButton(this, rowIndex, buttons.get(index + 4 * rowIndex), remotes.getParameter(rowIndex), control); + final int shiftOffset = rowIndex + 3; + bindParamButton( + shiftLayer, shiftOffset, buttons.get(index + 4 * rowIndex), remotes.getParameter(shiftOffset), control); + } + } + + private void bindParamButton(final Layer layer, final int index, final XoneRgbButton button, + final RemoteControl parameter, final TrackSpecControl control) { + parameter.markInterested(); + parameter.value().markInterested(); + parameter.discreteValueCount().markInterested(); + parameter.exists().markInterested(); + button.bindPressed(layer, () -> RemotesLayer.toggleParameter(parameter)); + button.bindLight(layer, () -> RemotesLayer.getParamState(index, parameter, control)); + } + + @Override + protected void onActivate() { + super.onActivate(); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + shiftLayer.setIsActive(false); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/ViewControl.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/ViewControl.java index 083d3f3a..adca363c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/ViewControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/ViewControl.java @@ -13,6 +13,7 @@ import com.bitwig.extension.controller.api.PinnableCursorDevice; 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.framework.di.Component; import com.bitwig.extensions.framework.di.PostConstruct; @@ -37,11 +38,13 @@ public class ViewControl { private final SceneBank sceneBank; private final SceneFocus sceneFocus; private BooleanValueObject controlsAnalogLab; + private final Track rootTrack; public static final int NUM_PADS_TRACK = 8; public ViewControl(final ControllerHost host) { mixerTrackBank = host.createTrackBank(8, 2, 2); cursorTrack = host.createCursorTrack(2, 2); + rootTrack = host.getProject().getRootTrackGroup(); viewTrackBank = host.createTrackBank(4, 2, 3); viewTrackBank.followCursorTrack(cursorTrack); @@ -64,9 +67,11 @@ public ViewControl(final ControllerHost host) { cursorClip.clipLauncherSlot().name().markInterested(); arrangerClip.exists().markInterested(); - 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.isWindowOpen().markInterested(); @@ -103,6 +108,10 @@ void init() { cursorTrack.hasPrevious().markInterested(); } + public void stopAllClips() { + rootTrack.stop(); + } + private void setUpFollowArturiaDevice(final ControllerHost host) { final DeviceMatcher arturiaMatcher = host.createVST3DeviceMatcher(ANALOG_LAB_V_DEVICE_ID); final DeviceBank matcherBank = cursorTrack.createDeviceBank(1); diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/display/ContextScreen.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/display/ContextScreen.java index 27b5c089..6d9c0608 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/display/ContextScreen.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk3/display/ContextScreen.java @@ -1,6 +1,7 @@ package com.bitwig.extensions.controllers.arturia.keylab.mk3.display; import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.ControllerHost; import com.bitwig.extension.controller.api.CursorRemoteControlsPage; import com.bitwig.extension.controller.api.CursorTrack; import com.bitwig.extension.controller.api.InternalHardwareLightState; @@ -25,6 +26,7 @@ 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.di.Inject; import com.bitwig.extensions.framework.values.LayoutType; @Component @@ -50,6 +52,10 @@ public class ContextScreen { private final Layer deviceLayer; private LayoutType panelLayout; private final SceneFocus sceneFocus; + private long sceneLaunchDownTime = -1; + + @Inject + private ControllerHost host; private enum ControlMode { MIXER, @@ -213,13 +219,25 @@ private void setUpContextButtons(final ViewControl viewControl, final KeylabHard final RgbButton contextButton7 = hwElements.getContextButton(6); contextButton7.bindPressed(mainLayer, () -> cursorTrack.mute().toggle()); final RgbButton contextButton8 = hwElements.getContextButton(7); - contextButton8.bindIsPressed( - mainLayer, pressed -> { - if (pressed) { - viewControl.launchScene(); - } - sceneButtonPressed = pressed; - }); + contextButton8.bindIsPressed(mainLayer, pressed -> sceneLaunching(viewControl, pressed)); + } + + private void sceneLaunching(final ViewControl viewControl, final Boolean pressed) { + if (pressed) { + sceneLaunchDownTime = System.currentTimeMillis(); + host.scheduleTask(this::delayedStopAll, 500); + } else if (sceneLaunchDownTime > 0) { + viewControl.launchScene(); + sceneLaunchDownTime = -1L; + } + sceneButtonPressed = pressed; + } + + private void delayedStopAll() { + if (sceneLaunchDownTime > 0) { + viewControl.stopAllClips(); + sceneLaunchDownTime = -1L; + } } private void toEncoderTrackControl() { diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayIntValueBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayIntValueBinding.java index ae72ff10..de924f1c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayIntValueBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayIntValueBinding.java @@ -11,7 +11,7 @@ public RingDisplayIntValueBinding(final IntValueObject source, final RingDisplay source.addValueObserver(this::valueChanged); } - private void valueChanged(final int oldValue, final int newValue) { + private void valueChanged(final int newValue) { if (isActive()) { getTarget().sendValue(calcValue(), false); } diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/RingParameterDisplaySlotBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/RingParameterDisplaySlotBinding.java index 27ac3970..0b1d8599 100644 --- a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/RingParameterDisplaySlotBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/RingParameterDisplaySlotBinding.java @@ -11,14 +11,14 @@ * enabled or not. */ public class RingParameterDisplaySlotBinding extends RingDisplayBinding implements ResetableBinding { - + private int lastValue; private boolean lastEnableValue = false; private boolean exists = false; - + public RingParameterDisplaySlotBinding(final ParamPageSlot source, final RingDisplay target) { super(target, source, RingDisplayType.FILL_LR_0); - source.getRingValue().addValueObserver((oldValue, newValue) -> { + source.getRingValue().addValueObserver(newValue -> { valueChange(source.getRingDisplayType().getOffset() + newValue); }); source.getExistsValue().addValueObserver(exists -> { @@ -28,26 +28,26 @@ public RingParameterDisplaySlotBinding(final ParamPageSlot source, final RingDis source.getEnabledValue().addValueObserver(this::handleEnabled); lastValue = source.getRingDisplayType().getOffset() + source.getRingValue().get(); } - + private void handleExists(final boolean exists) { this.exists = exists; if (isActive()) { update(); } } - + private void handleEnabled(final boolean enableValue) { lastEnableValue = enableValue; if (isActive()) { update(); } } - + @Override public void reset() { update(); } - + public void update() { if (isActive()) { lastValue = getSource().getRingValue().get() + getSource().getRingDisplayType().getOffset(); @@ -55,7 +55,7 @@ public void update() { getTarget().sendValue(value, false); } } - + private void valueChange(final int value) { lastValue = value; if (isActive()) { @@ -63,17 +63,18 @@ private void valueChange(final int value) { getTarget().sendValue(newValue, false); } } - + @Override protected void activate() { - lastValue = (exists && lastEnableValue) ? getSource().getRingDisplayType() - .getOffset() + getSource().getRingValue().get() : 0; + lastValue = + (exists && lastEnableValue) ? getSource().getRingDisplayType().getOffset() + getSource().getRingValue() + .get() : 0; getTarget().sendValue(lastValue, false); } - + @Override protected int calcValue() { return 0; } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/FocusSlot.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/FocusSlot.java similarity index 86% rename from src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/FocusSlot.java rename to src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/FocusSlot.java index 79dd097e..158d4dd3 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/FocusSlot.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/FocusSlot.java @@ -1,4 +1,4 @@ -package com.bitwig.extensions.controllers.novation.launchpadpromk3; +package com.bitwig.extensions.controllers.novation.commonsmk3; import com.bitwig.extension.controller.api.BooleanValue; import com.bitwig.extension.controller.api.ClipLauncherSlot; @@ -9,33 +9,33 @@ public class FocusSlot { private final ClipLauncherSlot slot; private final int slotIndex; private final BooleanValue equalsCursorTrack; - + public FocusSlot(final Track track, final ClipLauncherSlot slot, final int slotIndex, - BooleanValue equalsCursorTrack) { + final BooleanValue equalsCursorTrack) { this.track = track; this.slot = slot; this.slotIndex = slotIndex; this.equalsCursorTrack = equalsCursorTrack; } - + public Track getTrack() { return track; } - + public ClipLauncherSlot getSlot() { return slot; } - + public int getSlotIndex() { return slotIndex; } - + public boolean isEmpty() { return !slot.hasContent().get(); } - + public boolean isCursorTrack() { return equalsCursorTrack.get(); } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/RgbState.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/RgbState.java index ebbe45c1..9c4b1ea8 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/RgbState.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/RgbState.java @@ -34,6 +34,7 @@ public class RgbState extends InternalHardwareLightState { public static final RgbState BLUE = RgbState.of(41); + public static final RgbState DIM_GREEN = RgbState.of(23); public static final RgbState GREEN = RgbState.of(21); public static final RgbState BLUE_LO = RgbState.of(43); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ViewCursorControl.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ViewCursorControl.java index 5090076a..50924a27 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ViewCursorControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ViewCursorControl.java @@ -15,7 +15,6 @@ import com.bitwig.extension.controller.api.Track; import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extension.controller.api.Transport; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.FocusSlot; import com.bitwig.extensions.framework.di.Component; import com.bitwig.extensions.framework.di.Inject; diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlMidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlMidiProcessor.java index 3ba93692..d70e51f0 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlMidiProcessor.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlMidiProcessor.java @@ -1,13 +1,5 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3; -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.RgbColor; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer.BaseMode; -import com.bitwig.extensions.framework.di.Activate; -import com.bitwig.extensions.framework.di.Component; -import com.bitwig.extensions.framework.di.Deactivate; -import com.bitwig.extensions.framework.time.TimedEvent; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -16,195 +8,225 @@ import java.util.function.Consumer; import java.util.function.IntConsumer; +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +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.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extension.controller.api.RelativeHardwareValueMatcher; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.AbstractLaunchControlExtensionDefinition; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.RgbColor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer.BaseMode; +import com.bitwig.extensions.framework.di.Activate; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.di.Deactivate; +import com.bitwig.extensions.framework.time.TimedEvent; + @Component public class LaunchControlMidiProcessor { - private static final String DEVICE_INQUIRY = "F0 7E 7F 06 01 F7"; - private static final String DEVICE_RESPONSE_HEADER = "f07e000602002029"; - private static final String NOVATION_HEADER = "F0 00 20 29 02 15 "; - private static final byte[] TEXT_CONFIG_COMMAND = - {(byte) 0xF0, 0x00, 0x20, 0x29, 0x02, 0x15, 0x04, 0x00, 0x00, (byte) 0xF7}; - private static final int[] ROW_CC_IDS = {0x45, 0x48, 0x49}; - private static final String COLOR_RGB = "F0 00 20 29 02 15 01 53 %02X %02X %02X %02X F7"; - - private final ControllerHost host; - private final MidiIn midiIn; - private final MidiOut midiOut; - private Runnable hwUpdater; - protected final Queue timedEvents = new ConcurrentLinkedQueue<>(); - private final List> modeListeners = new ArrayList<>(); - private final List startListeners = new ArrayList<>(); - private boolean init = false; - - public LaunchControlMidiProcessor(final ControllerHost host) { - this.host = host; - this.midiIn = host.getMidiInPort(0); - this.midiOut = host.getMidiOutPort(0); - midiIn.setMidiCallback(this::handleMidiIn); - midiIn.setSysexCallback(this::handleSysEx); - } - - @Activate - public void init() { - midiOut.sendSysex(DEVICE_INQUIRY); - } - - @Deactivate - public void exit() { - midiOut.sendMidi(0x9F, 0x0C, 0x00); - } - - public String getSysexHeader() { - return NOVATION_HEADER; - } - - private void setMixLayout() { - midiOut.sendMidi(0xB6, 0x1E, 0x01); - } - - private void setControlLayout() { - midiOut.sendMidi(0xB6, 0x1E, 0x02); - } - - private void setToRelative(final int row, final boolean on) { - midiOut.sendMidi(0xB6, ROW_CC_IDS[row], on ? 0x7F : 0x00); - } - - public void queueEvent(final TimedEvent event) { - timedEvents.add(event); - } - - public void setCcMatcher(final HardwareButton hwButton, final int ccNr, final int channel) { - hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccNr, 127)); - hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccNr, 0)); - } - - public RelativeHardwareValueMatcher createNonAcceleratedMatcher(final int ccNr) { - final RelativeHardwareValueMatcher stepDownMatcher = - midiIn.createRelativeValueMatcher("(status == 191 && data1 == %d && data2 > 64)".formatted(ccNr), -1); - final RelativeHardwareValueMatcher stepUpMatcher = - midiIn.createRelativeValueMatcher("(status == 191 && data1 == %d && data2 < 65)".formatted(ccNr), 1); - return host.createOrRelativeHardwareValueMatcher(stepDownMatcher, stepUpMatcher); - } - - public RelativeHardwareValueMatcher createAcceleratedMatcher(final int ccNr) { - return midiIn.createRelativeBinOffsetCCValueMatcher(0xF, ccNr, 200); - } - - public RelativeHardwarControlBindable createIncAction(final IntConsumer changeAction) { - final HardwareActionBindable incAction = host.createAction(() -> changeAction.accept(1), () -> "+"); - final HardwareActionBindable decAction = host.createAction(() -> changeAction.accept(-1), () -> "-"); - return host.createRelativeHardwareControlStepTarget(incAction, decAction); - } - - public void setAbsoluteCcMatcher(final AbsoluteHardwareControl control, final int ccNr, final int channel) { - control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(channel, ccNr)); - } - - private void handleMidiIn(final int status, final int data1, final int data2) { - //LaunchControlXlMk3Extension.println("MIDI => %02X %02X %02X", status, data1, data2); - if (status == 0xB6 && data1 == 0x1E) { - final BaseMode mode = BaseMode.toMode(data2); - if (mode != null) { - this.modeListeners.forEach(listener -> listener.accept(mode)); - if (mode == BaseMode.MIXER) { - setToRelative(0, false); - setToRelative(1, false); - setToRelative(2, true); + private static final String DEVICE_INQUIRY = "F0 7E 7F 06 01 F7"; + private static final String DEVICE_RESPONSE_HEADER = "f07e000602002029"; + private static String NOVATION_HEADER; + private String LAUNCH_CONFIRM_CODE = "f000202902%s027ff7"; + private static final byte[] COLOR_SYSEX = { + (byte) 0xF0, 0x00, 0x20, 0x29, 0x02, 0x15, 0x01, 0x53, 0x00, 0x00, 0x00, 0x00, (byte) 0xF7 + }; + private static final int[] ROW_CC_IDS = {0x45, 0x48, 0x49}; + + private final ControllerHost host; + private final MidiIn midiIn; + private final MidiOut midiOut; + private Runnable hwUpdater; + protected final Queue timedEvents = new ConcurrentLinkedQueue<>(); + private final List> modeListeners = new ArrayList<>(); + private final List startListeners = new ArrayList<>(); + private final List timedListeners = new ArrayList<>(); + private boolean init = false; + + public LaunchControlMidiProcessor(final ControllerHost host, + final AbstractLaunchControlExtensionDefinition definition) { + final String productId = definition.isXlVersion() ? "15" : "16"; + LaunchControlMk3Extension.println(" IS XL = %s", definition.isXlVersion()); + LAUNCH_CONFIRM_CODE = LAUNCH_CONFIRM_CODE.formatted(productId); + NOVATION_HEADER = "F0 00 20 29 02 %s ".formatted(productId); + COLOR_SYSEX[5] = (byte) (definition.isXlVersion() ? 0x15 : 0x16); + this.host = host; + this.midiIn = host.getMidiInPort(0); + this.midiOut = host.getMidiOutPort(0); + midiIn.setMidiCallback(this::handleMidiIn); + midiIn.setSysexCallback(this::handleSysEx); + } + + @Activate + public void init() { + midiOut.sendSysex(DEVICE_INQUIRY); + } + + @Deactivate + public void exit() { + midiOut.sendMidi(0x9F, 0x0C, 0x00); + } + + public String getSysexHeader() { + return NOVATION_HEADER; + } + + private void setMixLayout() { + midiOut.sendMidi(0xB6, 0x1E, 0x01); + } + + private void setControlLayout() { + midiOut.sendMidi(0xB6, 0x1E, 0x02); + } + + public void setToRelative(final int row, final boolean on) { + midiOut.sendMidi(0xB6, ROW_CC_IDS[row], on ? 0x7F : 0x00); + } + + public void queueEvent(final TimedEvent event) { + timedEvents.add(event); + } + + public void addTimedListener(final Runnable listener) { + timedListeners.add(listener); + } + + public void setCcMatcher(final HardwareButton hwButton, final int ccNr, final int channel) { + hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccNr, 127)); + hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccNr, 0)); + } + + public RelativeHardwareValueMatcher createNonAcceleratedMatcher(final int ccNr) { + final RelativeHardwareValueMatcher stepDownMatcher = + midiIn.createRelativeValueMatcher("(status == 191 && data1 == %d && data2 > 64)".formatted(ccNr), -1); + final RelativeHardwareValueMatcher stepUpMatcher = + midiIn.createRelativeValueMatcher("(status == 191 && data1 == %d && data2 < 65)".formatted(ccNr), 1); + return host.createOrRelativeHardwareValueMatcher(stepDownMatcher, stepUpMatcher); + } + + public RelativeHardwareValueMatcher createAcceleratedMatcher(final int ccNr) { + return midiIn.createRelativeBinOffsetCCValueMatcher(0xF, ccNr, 200); + } + + public RelativeHardwarControlBindable createIncAction(final IntConsumer changeAction) { + final HardwareActionBindable incAction = host.createAction(() -> changeAction.accept(1), () -> "+"); + final HardwareActionBindable decAction = host.createAction(() -> changeAction.accept(-1), () -> "-"); + return host.createRelativeHardwareControlStepTarget(incAction, decAction); + } + + public void setAbsoluteCcMatcher(final AbsoluteHardwareControl control, final int ccNr, final int channel) { + control.setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(channel, ccNr)); + } + + private void handleMidiIn(final int status, final int data1, final int data2) { + if (status == 0xB6 && data1 == 0x1E) { + final BaseMode mode = BaseMode.toMode(data2); + if (mode != null) { + this.modeListeners.forEach(listener -> listener.accept(mode)); + } + } + } + + public void sendRgb(final int index, final RgbColor color) { + if (!init) { + return; + } + COLOR_SYSEX[8] = (byte) (0x7F & index); + COLOR_SYSEX[9] = (byte) (0x7F & color.red()); + COLOR_SYSEX[10] = (byte) (0x7F & color.green()); + COLOR_SYSEX[11] = (byte) (0x7F & color.blue()); + + midiOut.sendSysex(COLOR_SYSEX); + } + + public void addModeListener(final Consumer listener) { + this.modeListeners.add(listener); + } + + public void addStartListener(final Runnable startAction) { + this.startListeners.add(startAction); + } + + private void handleSysEx(final String data) { + if (!data.endsWith("f7")) { + LaunchControlMk3Extension.println("Illegal Sysex Received : %s", data); + return; + } + if (data.startsWith(DEVICE_RESPONSE_HEADER)) { + final String[] values = extractValues(data, DEVICE_RESPONSE_HEADER.length(), 8); + LaunchControlMk3Extension.println("Device response : %s", Arrays.toString(values)); + startMidi(); + } else { + if (data.startsWith(LAUNCH_CONFIRM_CODE)) { + hwUpdater.run(); } else { - setToRelative(0, true); - setToRelative(1, true); - setToRelative(2, true); + LaunchControlMk3Extension.println(" SYSEX %s", data); } - } - } - } - - public void addModeListener(final Consumer listener) { - this.modeListeners.add(listener); - } - - public void addStartListener(final Runnable startAction) { - this.startListeners.add(startAction); - } - - private void handleSysEx(final String data) { - if (!data.endsWith("f7")) { - LaunchControlXlMk3Extension.println("Illegal Sysex Received : %s", data); - return; - } - if (data.startsWith(DEVICE_RESPONSE_HEADER)) { - final String[] values = extractValues(data, DEVICE_RESPONSE_HEADER.length(), 8); - LaunchControlXlMk3Extension.println("Device response : %s", Arrays.toString(values)); - startMidi(); - } else if (data.startsWith("f00020290215027ff7")) { - hwUpdater.run(); - } else { - LaunchControlXlMk3Extension.println(" SYSEX %s", data); - } - } - - private void startMidi() { - init = true; - startDawMode(); - setControlLayout(); - setMixLayout(); - startListeners.forEach(Runnable::run); - host.scheduleTask(this::handlePing, 50); - } - - private void handlePing() { - if (!timedEvents.isEmpty()) { - for (final TimedEvent event : timedEvents) { - event.process(); - if (event.isCompleted()) { - timedEvents.remove(event); + } + } + + private void startMidi() { + init = true; + startDawMode(); + setControlLayout(); + setMixLayout(); + startListeners.forEach(Runnable::run); + host.scheduleTask(this::handlePing, 50); + } + + private void handlePing() { + if (!timedEvents.isEmpty()) { + for (final TimedEvent event : timedEvents) { + event.process(); + if (event.isCompleted()) { + timedEvents.remove(event); + } } - } - } - host.scheduleTask(this::handlePing, 100); - } - - private void startDawMode() { - final String msg = NOVATION_HEADER + "02 7F F7"; - midiOut.sendSysex(msg); - } - - public void sendMidi(final int status, final int data1, final int data2) { - midiOut.sendMidi(status, data1, data2); - } - - public void sendSysExBytes(final byte[] data) { - if (!init) { - return; - } - midiOut.sendSysex(data); - } - - public void sendSysExString(final String data) { - if (!init) { - return; - } - //LaunchControlXlMk3Extension.println(" SEND %s", data); - midiOut.sendSysex(data); - } - - private static String[] extractValues(final String data, final int offset, final int count) { - final String[] result = new String[count]; - int location = offset; - for (int i = 0; i < count && location + 1 < data.length(); i++) { - result[i] = data.substring(location, location + 2).toUpperCase(); - location += 2; - } - return result; - } - - - public void registerUpdater(final Runnable hwUpdater) { - this.hwUpdater = hwUpdater; - } - - public void sendRgb(final int index, final RgbColor color) { - final String msg = COLOR_RGB.formatted(index, color.red(), color.green(), color.blue()); - midiOut.sendSysex(msg); - } + } + for (final Runnable timedHandler : timedListeners) { + timedHandler.run(); + } + host.scheduleTask(this::handlePing, 100); + } + + private void startDawMode() { + midiOut.sendSysex(NOVATION_HEADER + "02 7F F7"); + } + + public void sendMidi(final int status, final int data1, final int data2) { + midiOut.sendMidi(status, data1, data2); + } + + public void sendSysExBytes(final byte[] data) { + if (!init) { + return; + } + midiOut.sendSysex(data); + } + + public void sendSysExString(final String data) { + if (!init) { + return; + } + //LaunchControlXlMk3Extension.println(" SEND %s", data); + midiOut.sendSysex(data); + } + + private static String[] extractValues(final String data, final int offset, final int count) { + final String[] result = new String[count]; + int location = offset; + for (int i = 0; i < count && location + 1 < data.length(); i++) { + result[i] = data.substring(location, location + 2).toUpperCase(); + location += 2; + } + return result; + } + + + public void registerUpdater(final Runnable hwUpdater) { + this.hwUpdater = hwUpdater; + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlMk3Extension.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlMk3Extension.java new file mode 100644 index 00000000..ac82dfdd --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlMk3Extension.java @@ -0,0 +1,80 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Set; + +import com.bitwig.extension.controller.ControllerExtension; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.AbstractLaunchControlExtensionDefinition; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer.LcDawControlLayer; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer.LcMixerLayer; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer.SpecControl; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Context; + +public class LaunchControlMk3Extension extends ControllerExtension { + + private static ControllerHost debugHost; + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + + private final AbstractLaunchControlExtensionDefinition definition; + private HardwareSurface surface; + 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)); + } + } + + public static void showCallLocation(final String message) { + println("MSG: %s", message); + for (final StackTraceElement element : Thread.currentThread().getStackTrace()) { + final String s = element.toString(); + if (s.startsWith("com.bitwig.extensions") && !s.contains("showCallLocation")) { + println(" | %s ", s.replace("com.bitwig.extensions.controllers.novation.launchcontrolxlmk3", "")); + } + } + } + + public LaunchControlMk3Extension(final AbstractLaunchControlExtensionDefinition definition, + final ControllerHost host) { + super(definition, host); + this.definition = definition; + } + + public void init() { + debugHost = getHost(); + diContext = new Context(this, Set.of(definition.isXlVersion() ? "XLModel" : "LCModel")); + diContext.registerService(AbstractLaunchControlExtensionDefinition.class, definition); + surface = diContext.getService(HardwareSurface.class); + if (!definition.isXlVersion()) { + final SpecControl specControl = diContext.create(SpecControl.class); + final LcDawControlLayer dawControlLayer = diContext.getService(LcDawControlLayer.class); + final LcMixerLayer mixerLayer = diContext.getService(LcMixerLayer.class); + dawControlLayer.setSpecOverlay(specControl); + mixerLayer.setSpecOverlay(specControl); + } + diContext.activate(); + final Layers layers = diContext.getService(Layers.class); + for (final Layer l : layers.getLayers()) { + LaunchControlMk3Extension.println(" > " + l.getName()); + } + } + + @Override + public void exit() { + // Nothing right now + diContext.deactivate(); + } + + @Override + public void flush() { + surface.updateHardware(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlHwElements.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlHwElements.java index 54cb3456..7cdb5efb 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlHwElements.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlHwElements.java @@ -1,5 +1,12 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + import com.bitwig.extension.controller.api.HardwareButton; import com.bitwig.extension.controller.api.HardwareSlider; import com.bitwig.extension.controller.api.HardwareSurface; @@ -11,85 +18,85 @@ import com.bitwig.extensions.framework.di.Component; import com.bitwig.extensions.framework.values.BooleanValueObject; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; - @Component public class LaunchControlXlHwElements { - - private final LaunchRelativeEncoder[] relativeEncoders = new LaunchRelativeEncoder[24]; - private final LaunchAbsoluteEncoder[] absoluteEncoders = new LaunchAbsoluteEncoder[24]; - private final LaunchLight[] encoderLights = new LaunchLight[24]; - private final LaunchButton[] rowButtons = new LaunchButton[16]; - - private final HardwareSlider[] sliders = new HardwareSlider[8]; - private final HardwareButton shiftButton; - private final Map launchButtons = new HashMap<>(); - private final BooleanValueObject shiftState = new BooleanValueObject(); - - public LaunchControlXlHwElements(final HardwareSurface surface, final LaunchControlMidiProcessor midiProcessor) { - - shiftButton = surface.createHardwareButton("SHIFT_BUTTON"); - midiProcessor.setCcMatcher(shiftButton, 0x3F, 0x6); - shiftButton.isPressed().addValueObserver(pressed -> shiftState.set(pressed)); - Arrays.stream(CcConstValues.values()) - .forEach(state -> launchButtons.put(state, new LaunchButton(state, surface, midiProcessor))); - - for (int i = 0; i < 24; i++) { - encoderLights[i] = new LaunchLight("ENC", i, 0xD + i, surface, midiProcessor); - relativeEncoders[i] = new LaunchRelativeEncoder(i, surface, midiProcessor, encoderLights[i]); - absoluteEncoders[i] = new LaunchAbsoluteEncoder(i, surface, midiProcessor, encoderLights[i]); - } - - for (int i = 0; i < 8; i++) { - sliders[i] = surface.createHardwareSlider("SLIDER_" + (i + 1)); - midiProcessor.setAbsoluteCcMatcher(sliders[i], 0x5 + i, 0xF); - rowButtons[i] = new LaunchButton("SOLO_ARM", i, 0x25 + i, 0, surface, midiProcessor); - rowButtons[i + 8] = new LaunchButton("SELECT_MUTE", i, 0x2D + i, 0, surface, midiProcessor); - } - midiProcessor.registerUpdater(this::handleForceUpdate); - } - - private void handleForceUpdate() { - for (int i = 0; i < 24; i++) { - encoderLights[i].forceUpdate(); - } - for (final LaunchButton rowButton : rowButtons) { - rowButton.forceUpdate(); - } - for (final LaunchButton button : launchButtons.values()) { - button.forceUpdate(); - } - } - - public BooleanValueObject getShiftState() { - return shiftState; - } - - public void bindShiftPressed(final Layer layer, final Consumer consumer) { - layer.bind(shiftButton, shiftButton.pressedAction(), () -> consumer.accept(true)); - layer.bind(shiftButton, shiftButton.releasedAction(), () -> consumer.accept(false)); - } - - public LaunchRelativeEncoder getRelativeEncoder(final int row, final int index) { - return relativeEncoders[row * 8 + index]; - } - - public LaunchAbsoluteEncoder getAbsoluteEncoder(final int row, final int index) { - return absoluteEncoders[row * 8 + index]; - } - - public LaunchButton getRowButtons(final int row, final int index) { - return rowButtons[row * 8 + index]; - } - - public HardwareSlider getSlider(final int index) { - return sliders[index]; - } - - public LaunchButton getButton(final CcConstValues constValue) { - return launchButtons.get(constValue); - } + + private final LaunchRelativeEncoder[] relativeEncoders = new LaunchRelativeEncoder[24]; + private final LaunchAbsoluteEncoder[] absoluteEncoders = new LaunchAbsoluteEncoder[24]; + private final LaunchLight[] encoderLights = new LaunchLight[24]; + private final LaunchButton[] rowButtons = new LaunchButton[16]; + + private final HardwareSlider[] sliders = new HardwareSlider[8]; + private final HardwareButton shiftButton; + private final Map launchButtons = new HashMap<>(); + private final BooleanValueObject shiftState = new BooleanValueObject(); + + public LaunchControlXlHwElements(final HardwareSurface surface, final LaunchControlMidiProcessor midiProcessor) { + shiftButton = surface.createHardwareButton("SHIFT_BUTTON"); + midiProcessor.setCcMatcher(shiftButton, 0x3F, 0x6); + shiftButton.isPressed().addValueObserver(pressed -> shiftState.set(pressed)); + Arrays.stream(CcConstValues.values()) + .forEach(state -> launchButtons.put(state, new LaunchButton(state, surface, midiProcessor))); + + for (int i = 0; i < 24; i++) { + encoderLights[i] = new LaunchLight("ENC", i, 0xD + i, surface, midiProcessor); + relativeEncoders[i] = new LaunchRelativeEncoder(i, surface, midiProcessor, encoderLights[i]); + absoluteEncoders[i] = new LaunchAbsoluteEncoder(i, surface, midiProcessor, encoderLights[i]); + } + + for (int i = 0; i < 8; i++) { + sliders[i] = surface.createHardwareSlider("SLIDER_" + (i + 1)); + midiProcessor.setAbsoluteCcMatcher(sliders[i], 0x5 + i, 0xF); + rowButtons[i] = new LaunchButton("SOLO_ARM", i, 0x25 + i, 0, surface, midiProcessor); + rowButtons[i + 8] = new LaunchButton("SELECT_MUTE", i, 0x2D + i, 0, surface, midiProcessor); + } + midiProcessor.registerUpdater(this::handleForceUpdate); + } + + private void handleForceUpdate() { + for (int i = 0; i < 24; i++) { + encoderLights[i].forceUpdate(); + } + for (final LaunchButton rowButton : rowButtons) { + rowButton.forceUpdate(); + } + for (final LaunchButton button : launchButtons.values()) { + button.forceUpdate(); + } + } + + public BooleanValueObject getShiftState() { + return shiftState; + } + + public void bindShiftPressed(final Layer layer, final Consumer consumer) { + layer.bind(shiftButton, shiftButton.pressedAction(), () -> consumer.accept(true)); + layer.bind(shiftButton, shiftButton.releasedAction(), () -> consumer.accept(false)); + } + + public LaunchRelativeEncoder getRelativeEncoder(final int row, final int index) { + return relativeEncoders[row * 8 + index]; + } + + public LaunchAbsoluteEncoder getAbsoluteEncoder(final int row, final int index) { + return absoluteEncoders[row * 8 + index]; + } + + public LaunchButton getRowButtons(final int row, final int index) { + return rowButtons[row * 8 + index]; + } + + public List getButtons(final int row) { + final ArrayList buttons = new ArrayList<>(); + buttons.addAll(Arrays.asList(rowButtons).subList(row * 8, 8 + row * 8)); + return buttons; + } + + public HardwareSlider getSlider(final int index) { + return sliders[index]; + } + + public LaunchButton getButtons(final CcConstValues constValue) { + return launchButtons.get(constValue); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlMk3Extension.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlMk3Extension.java deleted file mode 100644 index 4056e49d..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlMk3Extension.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3; - -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; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -public class LaunchControlXlMk3Extension extends ControllerExtension { - - private static ControllerHost debugHost; - private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); - - private final LaunchControlXlMk3ExtensionDefinition definition; - private HardwareSurface surface; - 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)); - } - } - - public LaunchControlXlMk3Extension(final LaunchControlXlMk3ExtensionDefinition definition, - final ControllerHost host) { - super(definition, host); - this.definition = definition; - } - - public void init() { - debugHost = getHost(); - diContext = new Context(this); - surface = diContext.getService(HardwareSurface.class); - diContext.activate(); - } - - @Override - public void exit() { - // Nothing right now - diContext.deactivate(); - } - - @Override - public void flush() { - surface.updateHardware(); - } - -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlMk3ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlMk3ExtensionDefinition.java deleted file mode 100644 index 083541cb..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchControlXlMk3ExtensionDefinition.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3; - -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 LaunchControlXlMk3ExtensionDefinition extends ControllerExtensionDefinition { - private static final UUID DRIVER_ID = UUID.fromString("cdee004a-1503-487c-bc13-a8311bf1724b"); - - public LaunchControlXlMk3ExtensionDefinition() { - } - - @Override - public String getName() { - return "Launch Control XL Mk3"; - } - - @Override - public String getAuthor() { - return "Bitwig"; - } - - @Override - public String getVersion() { - return "0.1"; - } - - @Override - public UUID getId() { - return DRIVER_ID; - } - - @Override - public String getHardwareVendor() { - return "Novation"; - } - - @Override - public String getHardwareModel() { - return "Launch Control XL Mk3"; - } - - @Override - public int getRequiredAPIVersion() { - return 24; - } - - @Override - public String getHelpFilePath() { - return "Controllers/Novation/Launch Control XL Mk3.pdf"; - } - - @Override - public int getNumMidiInPorts() { - return 1; - } - - @Override - public int getNumMidiOutPorts() { - return 1; - } - - @Override - public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, - final PlatformType platformType) { - if (platformType == PlatformType.WINDOWS) { - list.add(new String[]{"MIDIIN2 (LCXL3 1 MIDI)"}, new String[]{"MIDIOUT2 (LCXL3 1 MIDI)"}); - } else if (platformType == PlatformType.MAC) { - list.add(new String[]{"LCXL3 1 DAW Out"}, new String[]{"LCXL3 1 DAW In"}); - } else if (platformType == PlatformType.LINUX) { - list.add(new String[]{"LCXL3 1 LCXL3 1 DAW Out"}, new String[]{"LCXL3 1 LCXL3 1 DAW In"}); - } - } - - @Override - public LaunchControlXlMk3Extension createInstance(final ControllerHost host) { - return new LaunchControlXlMk3Extension(this, host); - } -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchViewControl.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchViewControl.java index fc1f6317..d7441cce 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchViewControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/LaunchViewControl.java @@ -6,6 +6,7 @@ import com.bitwig.extension.controller.api.SendBank; import com.bitwig.extension.controller.api.Track; import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.AbstractLaunchControlExtensionDefinition; import com.bitwig.extensions.framework.di.Component; @Component @@ -19,8 +20,8 @@ public class LaunchViewControl { private final TrackBank singleTrackBank; private final Track singleTrack; - public LaunchViewControl(final ControllerHost host) { - trackBank = host.createTrackBank(8, 2, 1); + public LaunchViewControl(final ControllerHost host, final AbstractLaunchControlExtensionDefinition definition) { + trackBank = host.createTrackBank(8, definition.isXlVersion() ? 2 : 1, 1); refSendBank = trackBank.getItemAt(0).sendBank(); for (int i = 0; i < 8; i++) { prepareTrack(trackBank.getItemAt(i)); @@ -88,4 +89,16 @@ public boolean canScrollBy(final int inc) { final int index = singleTrackBank.cursorIndex().get() + inc; return index >= 0 && index < singleTrackBank.itemCount().get(); } + + public boolean canNavLeft() { + return cursorTrack.hasPrevious().get(); + } + + public boolean canNavRight(final boolean shiftState) { + if (shiftState) { + return canScrollBy(8); + } + return cursorTrack.hasNext().get(); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/AbsoluteEncoderBinding.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/AbsoluteEncoderBinding.java index de6af294..75df23d5 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/AbsoluteEncoderBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/AbsoluteEncoderBinding.java @@ -1,19 +1,20 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings; +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; import com.bitwig.extension.controller.api.AbsoluteHardwareControlBinding; import com.bitwig.extension.controller.api.Parameter; -import com.bitwig.extension.controller.api.StringValue; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchAbsoluteEncoder; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; -public class AbsoluteEncoderBinding extends LauncherBinding { +public class AbsoluteEncoderBinding extends LauncherBinding { private int parameterValue; private boolean incoming = false; + private final LaunchAbsoluteEncoder knob; - public AbsoluteEncoderBinding(final Parameter parameter, final LaunchAbsoluteEncoder knob, - final DisplayControl displayControl, final StringValue trackName, final StringValue parameterName) { - super(knob.getTargetId(), parameter, knob, displayControl, trackName, parameterName); + public AbsoluteEncoderBinding(final Parameter parameter, final LaunchAbsoluteEncoder knob) { + super(knob.getId(), knob.getKnob(), parameter); + + this.knob = knob; parameter.value().addValueObserver(128, this::handleParameterValue); this.parameterValue = (int) (parameter.value().get() * 127); @@ -25,18 +26,18 @@ private void handleParameterValue(final int value) { if (incoming) { incoming = false; } else { - getTarget().updateValue(value); + knob.updateValue(value); } } } protected AbsoluteHardwareControlBinding getHardwareBinding() { - return getSource().addBinding(getTarget().getKnob()); + return getSource().addBinding(getTarget()); } @Override protected void updateValue() { - getTarget().updateValue(parameterValue); + knob.updateValue(parameterValue); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/ControlTargetId.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/ControlTargetId.java new file mode 100644 index 00000000..3293cd21 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/ControlTargetId.java @@ -0,0 +1,4 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings; + +public record ControlTargetId(int targetId) { +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/DisplayId.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/DisplayId.java index 4609a806..4dfaac74 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/DisplayId.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/DisplayId.java @@ -5,4 +5,19 @@ public record DisplayId(int index, DisplayControl display) { // + + @Override + public boolean equals(final Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + + final DisplayId displayId = (DisplayId) o; + return index == displayId.index; + } + + @Override + public int hashCode() { + return index; + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/FixedLightValueBinding.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/FixedLightValueBinding.java index 173c6380..3a2b3b93 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/FixedLightValueBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/FixedLightValueBinding.java @@ -9,7 +9,7 @@ public class FixedLightValueBinding extends Binding { private final RgbColor color; public FixedLightValueBinding(final LaunchLight target, final RgbColor color) { - super(color, color, target); + super(target.getLightId(), color, target); this.color = color; } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LauncherBinding.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LauncherBinding.java index e69f55d2..3e73a512 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LauncherBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LauncherBinding.java @@ -2,72 +2,19 @@ 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.novation.launchcontrolxlmk3.display.DisplayControl; import com.bitwig.extensions.framework.Binding; -public abstract class LauncherBinding extends Binding implements DisableBinding { - protected final int targetId; - protected String displayValue; - protected String parameterName; - protected String trackName; +public abstract class LauncherBinding extends Binding { protected HardwareBinding hwBinding; - protected final DisplayControl displayControl; - private boolean disabled; - public LauncherBinding(final int targetId, final Parameter parameter, final T target, - final DisplayControl displayControl, final StringValue trackName, final StringValue parameterName) { - super(parameter, parameter, target); - this.targetId = targetId; - this.displayControl = displayControl; - parameterName.addValueObserver(this::handleParameterName); - parameter.value().displayedValue().addValueObserver(this::handleDisplayValue); - trackName.addValueObserver(this::handleTrackName); - this.parameterName = parameterName.get(); - this.trackName = trackName.get(); - } - - private void handleTrackName(final String trackName) { - this.trackName = trackName; - if (isActive() && !disabled) { - displayControl.setText(targetId, 0, trackName); - displayControl.setText(targetId, 1, parameterName); - } - } - - private void handleParameterName(final String parameterName) { - this.parameterName = parameterName; - if (isActive() && !disabled) { - displayControl.setText(targetId, 1, parameterName); - } - } - - private void handleDisplayValue(final String value) { - this.displayValue = value; - if (isActive() && !disabled) { - displayControl.configureDisplay(targetId, 0x62); - displayControl.setText(targetId, 2, displayValue); - } + public LauncherBinding(final ControlTargetId targetId, final S control, final Parameter parameter) { + super(targetId, control, parameter); } protected abstract HardwareBinding getHardwareBinding(); protected abstract void updateValue(); - public void setDisabled(final boolean disabled) { - this.disabled = disabled; - if (isActive()) { - if (disabled) { - deactivate(); - displayControl.setText(targetId, 0, trackName); - displayControl.setText(targetId, 1, ""); - displayControl.setText(targetId, 2, ""); - } else { - activate(); - } - } - } - @Override protected void deactivate() { if (hwBinding != null) { @@ -78,27 +25,12 @@ protected void deactivate() { @Override protected void activate() { - if (disabled) { - return; - } if (hwBinding != null) { hwBinding.removeBinding(); } updateValue(); hwBinding = getHardwareBinding(); - displayControl.configureDisplay(targetId, 0x62); - fullTextUpdate(); } - private void fullTextUpdate() { - displayControl.setText(targetId, 0, trackName); - if (disabled) { - displayControl.setText(targetId, 1, ""); - displayControl.setText(targetId, 2, ""); - } else { - displayControl.setText(targetId, 1, parameterName); - displayControl.setText(targetId, 2, displayValue); - } - } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LightId.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LightId.java new file mode 100644 index 00000000..0b22ccc8 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LightId.java @@ -0,0 +1,4 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings; + +public record LightId(int index) { +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LightValueBindings.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LightValueBindings.java index 8741c354..78b718a7 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LightValueBindings.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/LightValueBindings.java @@ -14,7 +14,7 @@ public class LightValueBindings extends Binding implemen protected boolean disabled; public LightValueBindings(final Parameter parameter, final LaunchLight target, final GradientColor gradient) { - super(parameter, parameter, target); + super(target.getLightId(), parameter, target); this.gradient = gradient; parameter.value().addValueObserver(gradient.length(), this::handleValue); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/ParameterDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/ParameterDisplayBinding.java new file mode 100644 index 00000000..05688303 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/ParameterDisplayBinding.java @@ -0,0 +1,95 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings; + + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.framework.Binding; + +public class ParameterDisplayBinding extends Binding implements DisableBinding { + + private final DisplayControl display; + private final int targetId; + private long incTime = 0; + private String titleName; + private String parameterName; + private String displayValue; + private boolean disabled; + + public ParameterDisplayBinding(final DisplayId displayId, final StringValue titleName, final Parameter parameter) { + super(displayId, parameter, displayId); + this.display = displayId.display(); + this.targetId = displayId.index(); + + parameter.name().addValueObserver(this::handleParamNameChanged); + parameter.value().displayedValue().addValueObserver(this::handleDisplayValue); + titleName.addValueObserver(this::handleTrackName); + this.titleName = titleName.get(); + this.parameterName = parameter.name().get(); + } + + public void notifyInc() { + incTime = System.currentTimeMillis(); + } + + private void handleTrackName(final String trackName) { + this.titleName = trackName; + if (isActive() && !disabled) { + display.setText(targetId, 0, trackName); + display.setText(targetId, 1, parameterName); + } + } + + + private void handleParamNameChanged(final String value) { + this.parameterName = value; + if (isActive() && !disabled) { + display.setText(targetId, 1, parameterName); + final long diff = System.currentTimeMillis() - incTime; + if (diff < 200) { + display.showDisplay(targetId); + } + } + } + + private void handleDisplayValue(final String value) { + this.displayValue = value; + if (isActive() && !disabled) { + display.setText(targetId, 2, displayValue); + final long diff = System.currentTimeMillis() - incTime; + if (diff < 200) { + display.showDisplay(targetId); + } + } + } + + public void setDisabled(final boolean disabled) { + this.disabled = disabled; + if (isActive()) { + if (disabled) { + deactivate(); + display.setText(targetId, 0, titleName); + display.setText(targetId, 1, ""); + display.setText(targetId, 2, ""); + } else { + activate(); + } + } + } + + + @Override + protected void deactivate() { + } + + @Override + protected void activate() { + if (disabled) { + return; + } + display.setText(targetId, 0, titleName); + display.setText(targetId, 1, parameterName); + display.setText(targetId, 2, displayValue); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/RelativeDisplayControl.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/RelativeDisplayControl.java index e5e54534..e11674c6 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/RelativeDisplayControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/RelativeDisplayControl.java @@ -1,97 +1,100 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings; +import java.util.function.IntConsumer; + import com.bitwig.extension.controller.api.StringValue; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; import com.bitwig.extensions.framework.Binding; import com.bitwig.extensions.framework.values.BasicStringValue; -import java.util.function.IntConsumer; - public class RelativeDisplayControl extends Binding { - - private String paramName; - private final String title; - private long incTime = 0; - private String displayValue; - private final boolean pending = false; - private final int targetId; - private final DisplayControl display; - private final IntConsumer incAction; - private final Runnable activateAction; - - private RelativeDisplayControl(final DisplayId displayId, final String title, final StringValue paramName, - final StringValue paramValue, final IntConsumer incAction, final Runnable activateAction) { - // This has to become some kind of binding - super(displayId, displayId, incAction); - this.targetId = displayId.index(); - this.title = title; - this.paramName = paramName.get(); - paramValue.addValueObserver(this::handleDisplayValue); - paramName.addValueObserver(this::handleParamNameChanged); - this.displayValue = paramValue.get(); - this.display = displayId.display(); - this.incAction = incAction; - this.activateAction = activateAction; - } - - public RelativeDisplayControl(final int index, final DisplayControl display, final String title, - final String paramName, final StringValue paramValue, final IntConsumer incAction, - final Runnable activateAction) { - this( - new DisplayId(index, display), title, new BasicStringValue(paramName), paramValue, incAction, - activateAction); - } - - public RelativeDisplayControl(final int index, final DisplayControl display, final String title, - final StringValue paramName, final StringValue paramValue, final IntConsumer incAction, - final Runnable activateAction) { - this(new DisplayId(index, display), title, paramName, paramValue, incAction, activateAction); - } - - public RelativeDisplayControl(final int index, final DisplayControl display, final String title, - final String paramName, final StringValue paramValue, final IntConsumer incAction) { - this(new DisplayId(index, display), title, new BasicStringValue(paramName), paramValue, incAction, null); - } - - private void handleParamNameChanged(final String value) { - this.paramName = value; - if (isActive()) { - display.setText(targetId, 1, paramName); - final long diff = System.currentTimeMillis() - incTime; - if (diff < 200) { - display.showDisplay(targetId); - } - } - } - - private void handleDisplayValue(final String value) { - this.displayValue = value; - if (isActive()) { - display.setText(targetId, 2, displayValue); - final long diff = System.currentTimeMillis() - incTime; - if (diff < 200) { - display.showDisplay(targetId); - } - } - } - - public void handleInc(final int inc) { - incAction.accept(inc); - incTime = System.currentTimeMillis(); - } - - @Override - protected void deactivate() { - } - - @Override - protected void activate() { - display.setText(targetId, 0, title); - display.setText(targetId, 1, paramName); - display.setText(targetId, 2, displayValue); - - if (activateAction != null) { - activateAction.run(); - } - } + + private String paramName; + private final String title; + private long incTime = 0; + private String displayValue; + private final boolean pending = false; + private final int targetId; + private final DisplayControl display; + private final IntConsumer incAction; + private final Runnable activateAction; + + private RelativeDisplayControl(final DisplayId displayId, final String title, final StringValue paramName, + final StringValue paramValue, final IntConsumer incAction, final Runnable activateAction) { + // This has to become some kind of binding + super(displayId, displayId, incAction); + this.targetId = displayId.index(); + this.title = title; + this.paramName = paramName.get(); + paramValue.addValueObserver(this::handleDisplayValue); + paramName.addValueObserver(this::handleParamNameChanged); + this.displayValue = paramValue.get(); + this.display = displayId.display(); + this.incAction = incAction; + this.activateAction = activateAction; + } + + public RelativeDisplayControl(final int index, final DisplayControl display, final String title, + final String paramName, final StringValue paramValue, final IntConsumer incAction, + final Runnable activateAction) { + this( + new DisplayId(index, display), title, new BasicStringValue(paramName), paramValue, incAction, + activateAction); + } + + public RelativeDisplayControl(final int index, final DisplayControl display, final String title, + final StringValue paramName, final StringValue paramValue, final IntConsumer incAction, + final Runnable activateAction) { + this(new DisplayId(index, display), title, paramName, paramValue, incAction, activateAction); + } + + public RelativeDisplayControl(final int index, final DisplayControl display, final String title, + final String paramName, final StringValue paramValue, final IntConsumer incAction) { + this(new DisplayId(index, display), title, new BasicStringValue(paramName), paramValue, incAction, null); + } + + private void handleParamNameChanged(final String value) { + this.paramName = value; + if (isActive()) { + display.setText(targetId, 1, paramName); + final long diff = System.currentTimeMillis() - incTime; + if (diff < 200) { + display.showDisplay(targetId); + } + } + } + + private void handleDisplayValue(final String value) { + this.displayValue = value; + if (isActive()) { + display.setText(targetId, 2, displayValue); + final long diff = System.currentTimeMillis() - incTime; + if (diff < 200) { + display.showDisplay(targetId); + } + } + } + + public void handleInc(final int inc) { + incAction.accept(inc); + incTime = System.currentTimeMillis(); + } + + @Override + protected void deactivate() { + display.setText(targetId, 0, ""); + display.setText(targetId, 1, ""); + display.setText(targetId, 2, ""); + } + + @Override + protected void activate() { + display.setText(targetId, 0, title); + display.setText(targetId, 1, paramName); + display.setText(targetId, 2, displayValue); + + if (activateAction != null) { + activateAction.run(); + } + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/RelativeEncoderBinding.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/RelativeEncoderBinding.java index b47a0658..4bcb8c30 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/RelativeEncoderBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/RelativeEncoderBinding.java @@ -2,16 +2,17 @@ import com.bitwig.extension.controller.api.HardwareBinding; import com.bitwig.extension.controller.api.Parameter; -import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchRelativeEncoder; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; -public class RelativeEncoderBinding extends LauncherBinding { +public class RelativeEncoderBinding extends LauncherBinding { - public RelativeEncoderBinding(final Parameter parameter, final LaunchRelativeEncoder knob, - final DisplayControl displayControl, final StringValue trackName, final StringValue parameterName) { - super(knob.getTargetId(), parameter, knob, displayControl, trackName, parameterName); + private final LaunchRelativeEncoder encoder; + + public RelativeEncoderBinding(final Parameter parameter, final LaunchRelativeEncoder encoder) { + super(encoder.getId(), encoder.getEncoder(), parameter); parameter.discreteValueCount().addValueObserver(this::handleSteps); + this.encoder = encoder; } private void handleSteps(final int discreteSteps) { @@ -22,7 +23,7 @@ private void handleSteps(final int discreteSteps) { @Override protected HardwareBinding getHardwareBinding() { - return getSource().addBinding(getTarget().getEncoder()); + return getTarget().addBinding(getSource()); } @Override @@ -32,6 +33,6 @@ protected void updateValue() { @Override protected void activate() { super.activate(); - getTarget().setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 64); + encoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 64); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/SegmentDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/SegmentDisplayBinding.java index 52fbf4d2..f74181cf 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/SegmentDisplayBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/SegmentDisplayBinding.java @@ -34,14 +34,14 @@ private boolean isBlocked() { private void handleNameUpdate(final String name) { this.name = name; if (isActive() && !isBlocked()) { - getTarget().show2Lines(title, name); + getTarget().show2LinesBuffered(title, name); } } private void handleTitleUpdate(final String name) { this.title = name; if (isActive() && !isBlocked()) { - getTarget().show2Lines(title, name); + getTarget().show2LinesBuffered(title, name); } } @@ -51,6 +51,6 @@ protected void deactivate() { @Override protected void activate() { - getTarget().show2Lines(title, name); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/SliderBinding.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/SliderBinding.java index e59da43f..7ea8a3f4 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/SliderBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/bindings/SliderBinding.java @@ -3,18 +3,16 @@ import com.bitwig.extension.controller.api.AbsoluteHardwareControl; import com.bitwig.extension.controller.api.AbsoluteHardwareControlBinding; import com.bitwig.extension.controller.api.Parameter; -import com.bitwig.extension.controller.api.StringValue; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; public class SliderBinding extends LauncherBinding { - public SliderBinding(final int index, final Parameter parameter, final AbsoluteHardwareControl control, - final DisplayControl displayControl, final StringValue trackName, final StringValue parameterName) { - super(index + 0x5, parameter, control, displayControl, trackName, parameterName); + public SliderBinding(final ControlTargetId targetId, final Parameter parameter, + final AbsoluteHardwareControl control) { + super(targetId, control, parameter); //index + 0x05 } protected AbsoluteHardwareControlBinding getHardwareBinding() { - return getSource().addBinding(getTarget()); + return getTarget().addBinding(getSource()); } @Override diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchAbsoluteEncoder.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchAbsoluteEncoder.java index 02c2c8e5..274d6cd4 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchAbsoluteEncoder.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchAbsoluteEncoder.java @@ -1,14 +1,12 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control; import com.bitwig.extension.controller.api.AbsoluteHardwareKnob; -import com.bitwig.extension.controller.api.HardwareBinding; import com.bitwig.extension.controller.api.HardwareSurface; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.ControlTargetId; public class LaunchAbsoluteEncoder extends LaunchKnob { private final AbsoluteHardwareKnob knob; - private HardwareBinding hwBinding; - public LaunchAbsoluteEncoder(final int index, final HardwareSurface surface, final LaunchControlMidiProcessor midiProcessor, final LaunchLight light) { @@ -25,6 +23,10 @@ public int getCcNr() { return ccNr; } + public ControlTargetId getId() { + return new ControlTargetId(index + 0xD); + } + public void updateValue(final int value) { midiProcessor.sendMidi(0xBF, ccNr, value); } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchKnob.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchKnob.java index 36c269bc..b61213ff 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchKnob.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchKnob.java @@ -1,6 +1,7 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.ControlTargetId; public abstract class LaunchKnob { @@ -22,6 +23,10 @@ public int getTargetId() { return 0xD + index; } + public ControlTargetId getId() { + return new ControlTargetId(getTargetId()); + } + public LaunchLight getLight() { return light; } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchLight.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchLight.java index dfacd81c..e0d43d28 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchLight.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchLight.java @@ -5,6 +5,7 @@ import com.bitwig.extension.controller.api.MultiStateHardwareLight; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.LightId; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.RgbColor; public class LaunchLight { @@ -13,12 +14,14 @@ public class LaunchLight { private final int midiId; private final LaunchControlMidiProcessor midiProcessor; private RgbColor lastValue = RgbColor.OFF; + private final LightId lightId; public LaunchLight(final String name, final int index, final int midiId, final HardwareSurface surface, final LaunchControlMidiProcessor midiProcessor) { this.midiId = midiId; this.index = index; this.midiProcessor = midiProcessor; + this.lightId = new LightId(index); final MultiStateHardwareLight light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + index); light.state().onUpdateHardware(this::updateStateCC); } @@ -42,6 +45,10 @@ private void updateStateCC(final InternalHardwareLightState state) { } } + public LightId getLightId() { + return lightId; + } + public void sendRgbColor(final RgbColor color) { this.lastValue = color; midiProcessor.sendRgb(index + 0xD, color); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchRelativeEncoder.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchRelativeEncoder.java index 05af798b..a6a93791 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchRelativeEncoder.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/control/LaunchRelativeEncoder.java @@ -1,84 +1,96 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control; -import com.bitwig.extension.controller.api.*; +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.RelativeHardwareValueMatcher; +import com.bitwig.extension.controller.api.SettableRangedValue; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.ControlTargetId; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.RelativeHardwareControlToRangedValueBinding; -import java.util.function.IntConsumer; - public class LaunchRelativeEncoder extends LaunchKnob { - - private final RelativeHardwareKnob encoder; - private final RelativeHardwareValueMatcher nonAcceleratedMatchers; - private final RelativeHardwareValueMatcher acceleratedMatchers; - private final EncoderMode encoderMode = EncoderMode.ACCELERATED; - - public enum EncoderMode { - ACCELERATED, - NON_ACCELERATED - } - - public LaunchRelativeEncoder(final int index, final HardwareSurface surface, - final LaunchControlMidiProcessor midiProcessor, final LaunchLight light) { - super(index, 0x4D + index, midiProcessor, light); - encoder = surface.createRelativeHardwareKnob("ENCODER_" + index); - nonAcceleratedMatchers = midiProcessor.createNonAcceleratedMatcher(this.ccNr); - acceleratedMatchers = midiProcessor.createAcceleratedMatcher(this.ccNr); - setEncoderBehavior(encoderMode, 64); - } - - public LaunchLight getLight() { - return light; - } - - public void setEncoderBehavior(final EncoderMode mode) { - setEncoderBehavior(mode, mode == EncoderMode.ACCELERATED ? 64 : 1); - } - - public void setEncoderBehavior(final EncoderMode mode, final int stepSizeDivisor) { - if (mode == EncoderMode.ACCELERATED) { - encoder.setAdjustValueMatcher(acceleratedMatchers); - encoder.setStepSize(1.0 / stepSizeDivisor); - } else if (mode == EncoderMode.NON_ACCELERATED) { - encoder.setAdjustValueMatcher(nonAcceleratedMatchers); - encoder.setStepSize(1); - } - } - - public void bindIncrementAction(final Layer layer, final IntConsumer changeAction) { - layer.bind(encoder, midiProcessor.createIncAction(changeAction)); - } - - private RelativeHardwareControlToRangedValueBinding createEncoderToParamBinding(final SettableRangedValue param, - final double sensitivity) { - final RelativeHardwareControlToRangedValueBinding binding = - new RelativeHardwareControlToRangedValueBinding(encoder, param); - binding.setSensitivity(sensitivity); - return binding; - } - - private RelativeHardwareControlToRangedValueBinding createEncoderToParamBinding(final Parameter param) { - final RelativeHardwareControlToRangedValueBinding binding = - new RelativeHardwareControlToRangedValueBinding(encoder, param); - binding.setSensitivity(1); - return binding; - } - - public RelativeHardwareKnob getEncoder() { - return encoder; - } - - public void bind(final Layer layer, final RelativeHardwarControlBindable bindable) { - layer.bind(encoder, bindable); - } - - public void bindEmpty(final Layer layer) { - layer.bind(encoder, value -> { - }); - } - - public void bindParameter(final Layer layer, final Parameter parameter) { - layer.addBinding(createEncoderToParamBinding(parameter)); - } + + private final RelativeHardwareKnob encoder; + private final RelativeHardwareValueMatcher nonAcceleratedMatchers; + private final RelativeHardwareValueMatcher acceleratedMatchers; + private final EncoderMode encoderMode = EncoderMode.ACCELERATED; + + public enum EncoderMode { + ACCELERATED, + NON_ACCELERATED + } + + public LaunchRelativeEncoder(final int index, final HardwareSurface surface, + final LaunchControlMidiProcessor midiProcessor, final LaunchLight light) { + super(index, 0x4D + index, midiProcessor, light); + encoder = surface.createRelativeHardwareKnob("ENCODER_" + index); + nonAcceleratedMatchers = midiProcessor.createNonAcceleratedMatcher(this.ccNr); + acceleratedMatchers = midiProcessor.createAcceleratedMatcher(this.ccNr); + setEncoderBehavior(encoderMode, 64); + } + + @Override + public ControlTargetId getId() { + return new ControlTargetId(0xD + index); + } + + public LaunchLight getLight() { + return light; + } + + public void setEncoderBehavior(final EncoderMode mode) { + setEncoderBehavior(mode, mode == EncoderMode.ACCELERATED ? 64 : 1); + } + + public void setEncoderBehavior(final EncoderMode mode, final int stepSizeDivisor) { + if (mode == EncoderMode.ACCELERATED) { + encoder.setAdjustValueMatcher(acceleratedMatchers); + encoder.setStepSize(1.0 / stepSizeDivisor); + } else if (mode == EncoderMode.NON_ACCELERATED) { + encoder.setAdjustValueMatcher(nonAcceleratedMatchers); + encoder.setStepSize(1); + } + } + + public void bindIncrementAction(final Layer layer, final IntConsumer changeAction) { + layer.bind(encoder, midiProcessor.createIncAction(changeAction)); + } + + private RelativeHardwareControlToRangedValueBinding createEncoderToParamBinding(final SettableRangedValue param, + final double sensitivity) { + final RelativeHardwareControlToRangedValueBinding binding = + new RelativeHardwareControlToRangedValueBinding(encoder, param); + binding.setSensitivity(sensitivity); + return binding; + } + + private RelativeHardwareControlToRangedValueBinding createEncoderToParamBinding(final Parameter param) { + final RelativeHardwareControlToRangedValueBinding binding = + new RelativeHardwareControlToRangedValueBinding(encoder, param); + binding.setSensitivity(1); + return binding; + } + + public RelativeHardwareKnob getEncoder() { + return encoder; + } + + public void bind(final Layer layer, final RelativeHardwarControlBindable bindable) { + layer.bind(encoder, bindable); + } + + public void bindEmpty(final Layer layer) { + layer.bind( + encoder, value -> { + }); + } + + public void bindParameter(final Layer layer, final Parameter parameter) { + layer.addBinding(createEncoderToParamBinding(parameter)); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/AbstractLaunchControlExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/AbstractLaunchControlExtensionDefinition.java new file mode 100644 index 00000000..84b133dc --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/AbstractLaunchControlExtensionDefinition.java @@ -0,0 +1,37 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition; + +import com.bitwig.extension.controller.ControllerExtensionDefinition; + +public abstract class AbstractLaunchControlExtensionDefinition extends ControllerExtensionDefinition { + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "1.0"; + } + + @Override + public String getHardwareVendor() { + return "Novation"; + } + + @Override + public int getRequiredAPIVersion() { + return 24; + } + + @Override + public int getNumMidiInPorts() { + return 1; + } + + @Override + public int getNumMidiOutPorts() { + return 1; + } + + public abstract boolean isXlVersion(); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/LaunchControlExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/LaunchControlExtensionDefinition.java new file mode 100644 index 00000000..ca2bd981 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/LaunchControlExtensionDefinition.java @@ -0,0 +1,57 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMk3Extension; + +public class LaunchControlExtensionDefinition extends AbstractLaunchControlExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("cdee004a-1503-487c-bc13-a8311bf1724c"); + + public LaunchControlExtensionDefinition() { + } + + @Override + public boolean isXlVersion() { + return false; + } + + @Override + public String getName() { + return "Launch Control Mk3"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareModel() { + return "Launch Control Mk3"; + } + + @Override + public String getHelpFilePath() { + return "Controllers/Novation/Launch Control XL Mk3.pdf"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { + list.add(new String[] {"MIDIIN2 (LC3 1 MIDI)"}, new String[] {"MIDIOUT2 (LC3 1 MIDI)"}); + } else if (platformType == PlatformType.MAC) { + list.add(new String[] {"LC3 1 DAW Out"}, new String[] {"LC3 1 DAW In"}); + } else if (platformType == PlatformType.LINUX) { + list.add(new String[] {"LC3 1 LC3 1 DAW Out"}, new String[] {"LC3 1 LC3 1 DAW In"}); + } + } + + @Override + public LaunchControlMk3Extension createInstance(final ControllerHost host) { + return new LaunchControlMk3Extension(this, host); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/LaunchControlXlExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/LaunchControlXlExtensionDefinition.java new file mode 100644 index 00000000..5cfda5fa --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/definition/LaunchControlXlExtensionDefinition.java @@ -0,0 +1,57 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMk3Extension; + +public class LaunchControlXlExtensionDefinition extends AbstractLaunchControlExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("cdee004a-1503-487c-bc13-a8311bf1724b"); + + public LaunchControlXlExtensionDefinition() { + } + + @Override + public boolean isXlVersion() { + return true; + } + + @Override + public String getName() { + return "Launch Control XL Mk3"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareModel() { + return "Launch Control XL Mk3"; + } + + @Override + public String getHelpFilePath() { + return "Controllers/Novation/Launch Control XL Mk3.pdf"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { + list.add(new String[] {"MIDIIN2 (LCXL3 1 MIDI)"}, new String[] {"MIDIOUT2 (LCXL3 1 MIDI)"}); + } else if (platformType == PlatformType.MAC) { + list.add(new String[] {"LCXL3 1 DAW Out"}, new String[] {"LCXL3 1 DAW In"}); + } else if (platformType == PlatformType.LINUX) { + list.add(new String[] {"LCXL3 1 LCXL3 1 DAW Out"}, new String[] {"LCXL3 1 LCXL3 1 DAW In"}); + } + } + + @Override + public LaunchControlMk3Extension createInstance(final ControllerHost host) { + return new LaunchControlMk3Extension(this, host); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/display/DisplayControl.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/display/DisplayControl.java index 0e1d67a1..9fbfdd16 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/display/DisplayControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/display/DisplayControl.java @@ -1,113 +1,139 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; -import com.bitwig.extensions.framework.di.Component; - import java.util.HashMap; import java.util.Map; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.AbstractLaunchControlExtensionDefinition; +import com.bitwig.extensions.framework.di.Component; + @Component public class DisplayControl { - private static final byte[] TEXT_CONFIG_COMMAND = - {(byte) 0xF0, 0x00, 0x20, 0x29, 0x02, 0x15, 0x04, 0x00, 0x00, (byte) 0xF7}; - private final String textCommandHeader; - private final LaunchControlMidiProcessor midiProcessor; - private final DisplaySegment fixedDisplay; - private final DisplaySegment temporaryDisplay; - private FixDisplayState fixedState = FixDisplayState.TRACK; - private final Map targetConfigs = new HashMap<>(); - - public DisplayControl(final LaunchControlMidiProcessor processor) { - this.midiProcessor = processor; - this.fixedDisplay = new DisplaySegment(0x35, this); - this.temporaryDisplay = new DisplaySegment(0x36, this); - textCommandHeader = processor.getSysexHeader() + "06 "; - midiProcessor.addStartListener(this::initTemps); - } - - public void configureDisplay(final int targetId, final int config) { - targetConfigs.put(targetId, config); - TEXT_CONFIG_COMMAND[7] = (byte) targetId; - TEXT_CONFIG_COMMAND[8] = (byte) config; - midiProcessor.sendSysExBytes(TEXT_CONFIG_COMMAND); - } - - public void displayParamNames(final String... paramNames) { - this.fixedState = FixDisplayState.PARAM; - this.fixedDisplay.showParamInfo(paramNames); - } - - public void revertToFixed() { - this.temporaryDisplay.set2Lines(this.fixedDisplay); - this.temporaryDisplay.update2Lines(); - } - - public void fixDisplayUpdate(final int lineIndex, final String text, final long lastBlockTime) { - this.fixedDisplay.setLine(lineIndex, text); - final long diff = System.currentTimeMillis() - lastBlockTime; - if (diff > 1000) { - temporaryDisplay.setLine(lineIndex, text); - temporaryDisplay.update2Lines(); - } - if (this.fixedState == FixDisplayState.TRACK) { - this.fixedDisplay.update2Lines(); - } - } - - public void show2Line(final String line1, final String line2) { - temporaryDisplay.setLine(0, line1); - temporaryDisplay.setLine(1, line2); - temporaryDisplay.update2Lines(); - } - - public void showTempParamLines(final String title, final String name, final String value) { - temporaryDisplay.showParamValues(title, name, value); - } - - public DisplaySegment getTemporaryDisplay() { - return temporaryDisplay; - } - - public DisplaySegment getFixedDisplay() { - return fixedDisplay; - } - - public void showDisplay(final int targetId) { - TEXT_CONFIG_COMMAND[7] = (byte) targetId; - TEXT_CONFIG_COMMAND[8] = 0x7F; - midiProcessor.sendSysExBytes(TEXT_CONFIG_COMMAND); - } - - public void hideDisplay(final int targetId) { - TEXT_CONFIG_COMMAND[7] = (byte) targetId; - TEXT_CONFIG_COMMAND[8] = 0; - midiProcessor.sendSysExBytes(TEXT_CONFIG_COMMAND); - } - - public void setText(final int target, final int field, final String text) { - final StringBuilder msg = new StringBuilder(textCommandHeader); - msg.append("%02X ".formatted(target)); - msg.append("%02X ".formatted(field)); - final String validText = StringUtil.toAsciiDisplay(text, 16); - for (int i = 0; i < validText.length(); i++) { - msg.append("%02X ".formatted((int) validText.charAt(i))); - } - msg.append("F7"); - midiProcessor.sendSysExString(msg.toString()); - } - - public void initTemps() { - configureDisplay(0x21, 0x61); - configureDisplay(0x20, 0x61); - for (int i = 0; i < 24; i++) { - configureDisplay(0x05 + i, 0x62); - } - configureDisplay(0x22, 0x01); - configureDisplay(0x23, 0x61); - configureDisplay(0x24, 0x01); - configureDisplay(0x25, 0x01); - configureDisplay(0x26, 0x01); - configureDisplay(0x27, 0x01); - } - + private static final byte[] TEXT_CONFIG_COMMAND = + {(byte) 0xF0, 0x00, 0x20, 0x29, 0x02, 0x15, 0x04, 0x00, 0x00, (byte) 0xF7}; + private final String textCommandHeader; + private final LaunchControlMidiProcessor midiProcessor; + private final DisplaySegment fixedDisplay; + private final DisplaySegment temporaryDisplay; + private FixDisplayState fixedState = FixDisplayState.TRACK; + private final Map targetConfigs = new HashMap<>(); + private QueuedTextMessage waitingMessage = null; + + private record QueuedTextMessage(int targetId, int config, String line1, String line2) { + + } + + public DisplayControl(final LaunchControlMidiProcessor processor, + final AbstractLaunchControlExtensionDefinition definition) { + TEXT_CONFIG_COMMAND[5] = (byte) (definition.isXlVersion() ? 0x15 : 0x16); + this.midiProcessor = processor; + this.fixedDisplay = new DisplaySegment(0x35, this); + this.temporaryDisplay = new DisplaySegment(0x36, this); + textCommandHeader = processor.getSysexHeader() + "06 "; + midiProcessor.addStartListener(this::initTemps); + midiProcessor.addTimedListener(this::updateQuedMessages); + } + + private void updateQuedMessages() { + if (waitingMessage != null) { + configureDisplay(waitingMessage.targetId, waitingMessage.config); + setText(waitingMessage.targetId, 0, waitingMessage.line1); + setText(waitingMessage.targetId, 1, waitingMessage.line2); + showDisplay(waitingMessage.targetId); + waitingMessage = null; + } + } + + public void configureDisplay(final int targetId, final int config) { + targetConfigs.put(targetId, config); + TEXT_CONFIG_COMMAND[7] = (byte) targetId; + TEXT_CONFIG_COMMAND[8] = (byte) config; + midiProcessor.sendSysExBytes(TEXT_CONFIG_COMMAND); + } + + public void displayParamNames(final String... paramNames) { + this.fixedState = FixDisplayState.PARAM; + this.fixedDisplay.showParamInfo(paramNames); + } + + public void revertToFixed() { + this.temporaryDisplay.set2Lines(this.fixedDisplay); + this.temporaryDisplay.update2Lines(); + } + + public void fixDisplayUpdate(final int lineIndex, final String text, final long lastBlockTime) { + this.fixedDisplay.setLine(lineIndex, text); + final long diff = System.currentTimeMillis() - lastBlockTime; + if (diff > 1000) { + temporaryDisplay.setLine(lineIndex, text); + temporaryDisplay.update2Lines(); + } + if (this.fixedState == FixDisplayState.TRACK) { + this.fixedDisplay.update2Lines(); + } + } + + public void show2LineTemporary(final String line1, final String line2) { + temporaryDisplay.setLine(0, line1); + temporaryDisplay.setLine(1, line2); + temporaryDisplay.update2Lines(); + } + + public void cancelTemporary() { + hideDisplay(0x36); + } + + public void updateStatic() { + showDisplay(0x35); + } + + public DisplaySegment getTemporaryDisplay() { + return temporaryDisplay; + } + + public DisplaySegment getFixedDisplay() { + return fixedDisplay; + } + + public void showDisplay(final int targetId) { + TEXT_CONFIG_COMMAND[7] = (byte) targetId; + TEXT_CONFIG_COMMAND[8] = 0x7F; + midiProcessor.sendSysExBytes(TEXT_CONFIG_COMMAND); + } + + public void hideDisplay(final int targetId) { + TEXT_CONFIG_COMMAND[7] = (byte) targetId; + TEXT_CONFIG_COMMAND[8] = 0; + midiProcessor.sendSysExBytes(TEXT_CONFIG_COMMAND); + } + + public void setText(final int target, final int field, final String text) { + final StringBuilder msg = new StringBuilder(textCommandHeader); + msg.append("%02X ".formatted(target)); + msg.append("%02X ".formatted(field)); + final String validText = StringUtil.toAsciiDisplay(text, 16); + for (int i = 0; i < validText.length(); i++) { + msg.append("%02X ".formatted((int) validText.charAt(i))); + } + msg.append("F7"); + midiProcessor.sendSysExString(msg.toString()); + } + + public void initTemps() { + configureDisplay(0x21, 0x61); + configureDisplay(0x20, 0x61); + for (int i = 0; i < 24; i++) { + configureDisplay(0x05 + i, 0x62); + } + configureDisplay(0x22, 0x01); + configureDisplay(0x23, 0x61); + configureDisplay(0x24, 0x01); + configureDisplay(0x25, 0x01); + configureDisplay(0x26, 0x01); + configureDisplay(0x27, 0x01); + } + + public void queue2LineMessage(final int targetId, final int config, final String line1, final String line2) { + waitingMessage = new QueuedTextMessage(targetId, config, line1, line2); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/display/DisplaySegment.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/display/DisplaySegment.java index c73c6cae..26573be9 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/display/DisplaySegment.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/display/DisplaySegment.java @@ -36,6 +36,12 @@ public void show2Lines(final String line1, final String line2) { update2Lines(); } + public void show2LinesBuffered(final String line1, final String line2) { + stdLines[0] = line1; + stdLines[1] = line2; + control.queue2LineMessage(targetId, config, line1, line2); + } + public void update2Lines() { control.configureDisplay(targetId, config); control.setText(targetId, 0, stdLines[0]); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/AbstractDawControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/AbstractDawControlLayer.java new file mode 100644 index 00000000..beee2e98 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/AbstractDawControlLayer.java @@ -0,0 +1,63 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.SegmentDisplayBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +public abstract class AbstractDawControlLayer extends Layer { + + protected final CursorTrack cursorTrack; + protected final Remotes deviceRemotes; + protected final PinnableCursorDevice cursorDevice; + protected final BooleanValueObject shiftState; + protected final TransportHandler transportHandler; + protected final DisplayControl displayControl; + protected final LaunchViewControl viewControl; + + protected final SegmentDisplayBinding deviceDisplayBinding; + protected SegmentDisplayBinding selectTrackBinding; + + protected BasicStringValue deviceName = new BasicStringValue(""); + + public AbstractDawControlLayer(final Layers layers, final LaunchControlXlHwElements hwElements, + final LaunchViewControl viewControl, final DisplayControl displayControl, + final TransportHandler transportHandler, final ControllerHost host) { + super(layers, "DAW_CONTROL"); + this.transportHandler = transportHandler; + this.viewControl = viewControl; + this.displayControl = displayControl; + shiftState = hwElements.getShiftState(); + this.cursorTrack = viewControl.getCursorTrack(); + + cursorDevice = viewControl.getCursorDevice(); + cursorDevice.name().addValueObserver(deviceName::set); + cursorDevice.hasPrevious().markInterested(); + cursorDevice.hasNext().markInterested(); + deviceRemotes = new Remotes(cursorDevice); + + deviceDisplayBinding = new SegmentDisplayBinding( + this.deviceName, deviceRemotes.getDevicePageName(), + displayControl.getFixedDisplay()); + this.addBinding(deviceDisplayBinding); + } + + protected void navigateTracks(final int inc) { + if (inc > 0) { + cursorTrack.selectNext(); + } else { + cursorTrack.selectPrevious(); + } + selectTrackBinding.blockUpdate(); + deviceDisplayBinding.blockUpdate(); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/AbstractMixerLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/AbstractMixerLayer.java new file mode 100644 index 00000000..0b6e1d27 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/AbstractMixerLayer.java @@ -0,0 +1,154 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.Project; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.BasicIntegerValue; +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +public abstract class AbstractMixerLayer extends Layer { + protected final ButtonLayers buttonLayers; + protected final LaunchControlMidiProcessor midiProcessor; + protected final TransportHandler transportHandler; + protected final Project project; + protected final BooleanValueObject shiftState; + protected final LaunchViewControl viewControl; + protected final DisplayControl displayControl; + protected int armHeld = 0; + protected int soloHeld = 0; + protected BaseMode mode = BaseMode.MIXER; + protected final BasicIntegerValue selectedTrackIndex = new BasicIntegerValue(); + protected final RgbState[] trackColors = new RgbState[8]; + + protected final BasicStringValue fixedVolumeLabel = new BasicStringValue("Volume"); + protected final BasicStringValue fixedPanLabel = new BasicStringValue("Panning"); + + protected final Layer mixerLayer; + + + public AbstractMixerLayer(final Layers layers, final LaunchControlMidiProcessor midiProcessor, + final ControllerHost host, final LaunchViewControl viewControl, final LaunchControlXlHwElements hwElements, + final DisplayControl displayControl, final TransportHandler transportHandler, final ButtonLayers buttonLayers) { + super(layers, "MIXER"); + mixerLayer = new Layer(layers, "MIXER_LAYER"); + this.project = host.getProject(); + this.transportHandler = transportHandler; + this.midiProcessor = midiProcessor; + project.hasArmedTracks().markInterested(); + project.hasSoloedTracks().markInterested(); + this.displayControl = displayControl; + this.viewControl = viewControl; + this.shiftState = hwElements.getShiftState(); + this.buttonLayers = buttonLayers; + midiProcessor.addModeListener(this::handleModeChange); + } + + protected abstract void applyMode(); + + private void handleModeChange(final BaseMode baseMode) { + this.mode = baseMode; + applyMode(); + } + + protected void changeTrackColor(final int index, final int color) { + if (color == 1) { + trackColors[index] = RgbState.of(color); + } else { + trackColors[index] = RgbState.of(color).dim(); + } + } + + protected void selectTrack(final Track track) { + track.selectInMixer(); + } + + protected void toggleArm(final boolean pressed, final Track track) { + if (shiftState.get()) { + if (pressed) { + track.arm().toggle(); + } + } else if (pressed) { + armHeld++; + if (armHeld == 1) { + final boolean armed = track.arm().get(); + project.unarmAll(); + if (!armed) { + track.arm().toggle(); + } + } else { + track.arm().toggle(); + } + } else { + if (armHeld > 0) { + armHeld--; + } + } + } + + protected void toggleSolo(final boolean pressed, final Track track) { + if (shiftState.get()) { + if (pressed) { + track.solo().toggle(); + } + } else if (pressed) { + soloHeld++; + if (soloHeld == 1) { + track.solo().toggle(true); + } else { + track.solo().toggle(); + } + } else { + if (soloHeld > 0) { + soloHeld--; + } + } + } + + protected RgbState muteColor(final Track track) { + if (track.exists().get()) { + return track.mute().get() ? RgbState.ORANGE : RgbState.ORANGE_LO; + } + return RgbState.OFF; + } + + protected RgbState selectColor(final Track track, final int index) { + if (track.exists().get()) { + return index == selectedTrackIndex.get() ? RgbState.WHITE : trackColors[index]; + } + return RgbState.OFF; + } + + protected RgbState armColor(final Track track) { + if (track.exists().get()) { + return track.arm().get() ? RgbState.RED : RgbState.RED_LO; + } + return RgbState.OFF; + } + + protected RgbState soloColor(final Track track) { + if (track.exists().get()) { + return track.solo().get() ? RgbState.YELLOW : RgbState.YELLOW_LO; + } + return RgbState.OFF; + } + + + @Override + protected void onDeactivate() { + super.onDeactivate(); + } + + @Override + protected void onActivate() { + super.onActivate(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/ButtonLayers.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/ButtonLayers.java new file mode 100644 index 00000000..358dd136 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/ButtonLayers.java @@ -0,0 +1,37 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; + +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class ButtonLayers { + + private final Layer selectLayer; + private final Layer soloLayer; + private final Layer muteLayer; + private final Layer armLayer; + + public ButtonLayers(final Layers layers) { + selectLayer = new Layer(layers, "SELECT"); + soloLayer = new Layer(layers, "SOLO"); + armLayer = new Layer(layers, "ARM"); + muteLayer = new Layer(layers, "MUTE"); + } + + public Layer getSelectLayer() { + return selectLayer; + } + + public Layer getSoloLayer() { + return soloLayer; + } + + public Layer getMuteLayer() { + return muteLayer; + } + + public Layer getArmLayer() { + return armLayer; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/DawControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/DawControlLayer.java deleted file mode 100644 index 18109ee6..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/DawControlLayer.java +++ /dev/null @@ -1,404 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; - -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.CcConstValues; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.BooleanLightValueBinding; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.FixedLightValueBinding; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.RelativeDisplayControl; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.SegmentDisplayBinding; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchButton; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchRelativeEncoder; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.RgbColor; -import com.bitwig.extensions.framework.Layer; -import com.bitwig.extensions.framework.Layers; -import com.bitwig.extensions.framework.di.Component; -import com.bitwig.extensions.framework.values.BasicStringValue; -import com.bitwig.extensions.framework.values.BooleanValueObject; -import com.bitwig.extensions.framework.values.LayoutType; -import com.bitwig.extensions.framework.values.ValueObject; - -import java.util.function.IntConsumer; - -@Component -public class DawControlLayer extends Layer { - - - private final BeatTimeFormatter formatter; - private final PinnableCursorDevice cursorDevice; - private final BooleanValueObject shiftState; - private final DisplayControl display; - private final ValueObject panelLayout = new ValueObject<>(LayoutType.ARRANGER); - private boolean markerPositionChangePending = false; - - private final Remotes deviceRemotes; - private final CursorTrack cursorTrack; - private final Arranger arranger; - private final SceneBank sceneBank; - private final Scene focusScene; - private double scrubDistance; - private final Layer launcherLayer; - private final Layer arrangerLayer; - private final SegmentDisplayBinding selectTrackBinding; - private final SegmentDisplayBinding deviceDisplayBinding; - private final TransportHandler transportHandler; - private final ScrollbarModel horizontalScrollbarModel; - - final BasicStringValue zoomValue = new BasicStringValue(""); - final BasicStringValue zoomVerticalValue = new BasicStringValue(""); - final BasicStringValue cueMarkerValue = new BasicStringValue(""); - final BasicStringValue deviceName = new BasicStringValue(""); - final CueMarkerBank cueMarkerBank; - - public DawControlLayer(final Layers layers, final ControllerHost host, final LaunchControlXlHwElements hwElements, - final LaunchViewControl viewControl, final DisplayControl displayControl, - final TransportHandler transportHandler, final Application application) { - super(layers, "DAW"); - application.panelLayout().addValueObserver(layout -> this.panelLayout.set(LayoutType.toType(layout))); - this.launcherLayer = new Layer(layers, "LAUNCHER"); - this.arrangerLayer = new Layer(layers, "ARRANGER"); - this.formatter = host.createBeatTimeFormatter(":", 2, 1, 1, 0); - this.display = displayControl; - this.cursorTrack = viewControl.getCursorTrack(); - this.transportHandler = transportHandler; - sceneBank = viewControl.getTrackBank().sceneBank(); - focusScene = sceneBank.getScene(0); - sceneBank.setIndication(true); - cursorDevice = viewControl.getCursorDevice(); - cursorDevice.name().addValueObserver(deviceName::set); - cursorDevice.hasPrevious().markInterested(); - cursorDevice.hasNext().markInterested(); - shiftState = hwElements.getShiftState(); - deviceRemotes = new Remotes(cursorDevice); - this.arranger = host.createArranger(); - horizontalScrollbarModel = this.arranger.getHorizontalScrollbarModel(); - horizontalScrollbarModel.getContentPerPixel().addValueObserver(this::handleZoomLevel); - - cueMarkerBank = arranger.createCueMarkerBank(1); - deviceRemotes.bind(this, hwElements, displayControl); - transportHandler.bindTrackNavigation(this); - bindNavigation(hwElements); - bindTransport(hwElements); - - deviceDisplayBinding = new SegmentDisplayBinding( - this.deviceName, deviceRemotes.getDevicePageName(), - displayControl.getFixedDisplay()); - this.addBinding(deviceDisplayBinding); - selectTrackBinding = - new SegmentDisplayBinding("Select Track", cursorTrack.name(), displayControl.getTemporaryDisplay()); - this.addBinding(selectTrackBinding); - - configureZoomAndMarkers(); - } - - public static double roundToNearestPowerOfTwo(final double value) { - if (value <= 0) { - throw new IllegalArgumentException("Value must be greater than zero."); - } - final double log2 = Math.log(value) / Math.log(2); - final double roundedPower = Math.round(log2); - return Math.pow(2, roundedPower); - } - - private void handleZoomLevel(final double v) { - if (v <= 0) { - return; - } - this.scrubDistance = roundToNearestPowerOfTwo(80 * v); - } - - private void bindTransport(final LaunchControlXlHwElements hwElements) { - final LaunchRelativeEncoder playbackPosEncoder = hwElements.getRelativeEncoder(2, 0); - final LaunchRelativeEncoder loopStartEncoder = hwElements.getRelativeEncoder(2, 3); - final LaunchRelativeEncoder looEndEncoder = hwElements.getRelativeEncoder(2, 4); - final Transport transport = transportHandler.getTransport(); - final SettableBeatTimeValue playPosition = transport.getPosition(); - bindPosition(this, playPosition, playbackPosEncoder, "PlaybackPosition", false, true, RgbColor.BLUE_LOW); - - bindPosition( - this, transport.arrangerLoopStart(), loopStartEncoder, "Loop Start", true, false, RgbColor.BLUE_LOW); - bindPosition( - this, transport.arrangerLoopDuration(), looEndEncoder, "Loop Duration", true, false, RgbColor.BLUE_LOW); - - final LaunchRelativeEncoder zoomHorizontalEncoder = hwElements.getRelativeEncoder(2, 1); - arrangerLayer.addBinding(new FixedLightValueBinding(zoomHorizontalEncoder.getLight(), RgbColor.LOW_WHITE)); - - final RelativeDisplayControl zoomArrangerControl = getHorizontalZoomControl(zoomHorizontalEncoder); - zoomHorizontalEncoder.bindIncrementAction(arrangerLayer, zoomArrangerControl::handleInc); - this.addBinding(zoomArrangerControl); - - launcherLayer.addBinding(new FixedLightValueBinding(zoomHorizontalEncoder.getLight(), RgbColor.ORANGE)); - final RelativeDisplayControl trackScrollView = - new RelativeDisplayControl( - zoomHorizontalEncoder.getTargetId(), display, "Transport", "Select Track", cursorTrack.name(), - this::navigateTracks); - zoomHorizontalEncoder.bindIncrementAction(launcherLayer, trackScrollView::handleInc); - launcherLayer.addBinding(trackScrollView); - - // This is scene select - final LaunchRelativeEncoder zoomVerticalEncoder = hwElements.getRelativeEncoder(2, 2); - - arrangerLayer.addBinding(new FixedLightValueBinding(zoomVerticalEncoder.getLight(), RgbColor.LOW_WHITE)); - setUpVerticalZoomEncoder(arrangerLayer, zoomVerticalEncoder); - - launcherLayer.addBinding(new FixedLightValueBinding(zoomVerticalEncoder.getLight(), RgbColor.BLUE_LOW)); - setUpSceneEncoder(launcherLayer, zoomVerticalEncoder); - - final BasicStringValue loopActive = new BasicStringValue("On"); - transport.isArrangerLoopEnabled().addValueObserver(active -> loopActive.set(active ? "ON" : "OFF")); - final LaunchRelativeEncoder loopActiveEncoder = hwElements.getRelativeEncoder(2, 5); - final RelativeDisplayControl loopControl = - new RelativeDisplayControl( - loopActiveEncoder.getTargetId(), display, "Transport", "Loop", loopActive, - inc -> this.handleLoopOnOff(transport, inc)); - loopActiveEncoder.bindIncrementAction(this, loopControl::handleInc); - this.addBinding(loopControl); - this.addBinding(new BooleanLightValueBinding( - loopActiveEncoder.getLight(), transport.isArrangerLoopEnabled(), - RgbColor.BLUE_LOW, RgbColor.BLUE_DIM)); - - final LaunchRelativeEncoder markerSelectionEncoder = hwElements.getRelativeEncoder(2, 6); - this.addBinding(new FixedLightValueBinding(markerSelectionEncoder.getLight(), RgbColor.YELLOW)); - final RelativeDisplayControl cueMarkerControl = new RelativeDisplayControl( - markerSelectionEncoder.getTargetId(), display, "Transport", "Marker Select", cueMarkerValue, - this::handleCuePointSelection, - () -> markerSelectionEncoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 48)); - markerSelectionEncoder.bindIncrementAction(this, cueMarkerControl::handleInc); - this.addBinding(cueMarkerControl); - - final LaunchRelativeEncoder tempoEncoder = hwElements.getRelativeEncoder(2, 7); - this.addBinding(new FixedLightValueBinding(tempoEncoder.getLight(), RgbColor.BLUE_LOW)); - final RelativeDisplayControl tempoControl = - new RelativeDisplayControl( - tempoEncoder.getTargetId(), display, "Transport", "Tempo", transport.tempo().displayedValue(), - inc -> transport.tempo().incRaw(inc)); - tempoEncoder.bindIncrementAction(this, tempoControl::handleInc); - this.addBinding(tempoControl); - } - - private void navigateTracks(final int inc) { - if (inc > 0) { - cursorTrack.selectNext(); - } else { - cursorTrack.selectPrevious(); - } - selectTrackBinding.blockUpdate(); - deviceDisplayBinding.blockUpdate(); - } - - private void handleLoopOnOff(final Transport transport, final int inc) { - transport.isArrangerLoopEnabled().set(inc > 0); - } - - private void handleCuePointSelection(final int inc) { - if (inc < 0) { - cueMarkerBank.scrollBackwards(); - } else { - cueMarkerBank.scrollForwards(); - } - markerPositionChangePending = true; - } - - private RelativeDisplayControl getHorizontalZoomControl(final LaunchRelativeEncoder encoder) { - final IncrementDecelerator horizontalZoomIncrementor = - new IncrementDecelerator(inc -> handleHorizontalZoom(zoomValue, inc), 50); - final RelativeDisplayControl zoomArrangerControl = - new RelativeDisplayControl( - encoder.getTargetId(), display, "Transport", "Zoom Arranger", zoomValue, horizontalZoomIncrementor, - () -> encoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 64)); - return zoomArrangerControl; - } - - private void setUpVerticalZoomEncoder(final Layer layer, final LaunchRelativeEncoder encoder) { - final IncrementDecelerator verticalZoomIncrementor = - new IncrementDecelerator(inc -> handleVerticalZoom(zoomVerticalValue, inc), 60); - final RelativeDisplayControl zoomVerticalControl = - new RelativeDisplayControl( - encoder.getTargetId(), display, "Transport", "Zoom Tracks", zoomVerticalValue, verticalZoomIncrementor, - () -> encoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 32)); - encoder.bindIncrementAction(layer, zoomVerticalControl::handleInc); - layer.addBinding(zoomVerticalControl); - } - - private void setUpSceneEncoder(final Layer layer, final LaunchRelativeEncoder encoder) { - final IncrementDecelerator verticalZoomIncrementor = new IncrementDecelerator(this::handleSceneSelect, 60); - final RelativeDisplayControl zoomVerticalControl = - new RelativeDisplayControl( - encoder.getTargetId(), display, "Transport", "Scene Select", focusScene.name(), verticalZoomIncrementor, - () -> encoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 32)); - encoder.bindIncrementAction(layer, zoomVerticalControl::handleInc); - layer.addBinding(zoomVerticalControl); - } - - private void configureZoomAndMarkers() { - this.panelLayout.addValueObserver(this::handlePanelLayoutUpdate); - cursorTrack.name().addValueObserver(this::handleCursorTrackNameUpdate); - final CueMarker marker = cueMarkerBank.getItemAt(0); - marker.position().markInterested(); - marker.exists().addValueObserver(exists -> updateCueMarker(cueMarkerValue, marker.name().get(), exists)); - marker.name().addValueObserver(name -> updateCueMarker(cueMarkerValue, name, marker.exists().get())); - marker.position().addValueObserver(this::updateMarkerPosition); - } - - private void updateMarkerPosition(final double pos) { - if (markerPositionChangePending) { - markerPositionChangePending = false; - transportHandler.getTransport().getPosition().set(pos); - } - } - - private void handleCursorTrackNameUpdate(final String name) { - if (this.panelLayout.get() != LayoutType.ARRANGER) { - zoomValue.set(name); - } - } - - private void handlePanelLayoutUpdate(final LayoutType newValue) { - if (newValue == LayoutType.LAUNCHER) { - zoomValue.set(cursorTrack.name().get()); - zoomVerticalValue.set(focusScene.name().get()); - } - if (isActive()) { - launcherLayer.setIsActive(newValue == LayoutType.LAUNCHER); - arrangerLayer.setIsActive(newValue == LayoutType.ARRANGER); - } - } - - private void bindNavigation(final LaunchControlXlHwElements hwElements) { - final LaunchButton pageUpButton = hwElements.getButton(CcConstValues.PAGE_UP); - final LaunchButton pageDownButton = hwElements.getButton(CcConstValues.PAGE_DOWN); - pageUpButton.bindLight(this, this::canPageNavigateBackward); - pageDownButton.bindLight(this, this::canPageNavigateForward); - pageUpButton.bindRepeatHold(this, this::navigateBackward); - pageDownButton.bindRepeatHold(this, this::navigateForward); - } - - private void bindPosition(final Layer layer, final SettableBeatTimeValue position, - final LaunchRelativeEncoder encoder, final String label, final boolean hasMinimum, final boolean deceleration, - final RgbColor color) { - - final BasicStringValue transportPosition = new BasicStringValue(""); - position.addValueObserver(value -> transportPosition.set(position.getFormatted(formatter))); - - final IntConsumer valueModifier = hasMinimum ? inc -> incPositionBounded(position, inc * 4.0) : inc -> { - handlePositionIncrementWithFocus(position, inc); - }; - final RelativeDisplayControl positionControl = - new RelativeDisplayControl( - encoder.getTargetId(), display, "Transport", label, transportPosition, valueModifier); - encoder.bindIncrementAction(layer, positionControl::handleInc); - layer.addBinding(positionControl); - layer.addBinding(new FixedLightValueBinding(encoder.getLight(), color)); - } - - private void handlePositionIncrementWithFocus(final SettableBeatTimeValue position, final int inc) { - final double newPos = position.get() + (inc * scrubDistance); - horizontalScrollbarModel.zoomAtPosition(newPos, 0); - position.set(newPos); - } - - private void incPositionBounded(final SettableBeatTimeValue position, final double inc) { - final double newValue = position.get() + inc; - if (newValue >= 0.0) { - position.set(newValue); - } else { - position.set(0.0); - } - } - - private void updateCueMarker(final BasicStringValue cueMarkerValue, final String name, final boolean exists) { - if (exists) { - cueMarkerValue.set("Marker: %s".formatted(name)); - } else { - cueMarkerValue.set("No Markers"); - } - } - - private void handleHorizontalZoom(final BasicStringValue zoomValue, final int inc) { - if (this.panelLayout.get() == LayoutType.ARRANGER) { - final double newPos = transportHandler.getTransport().getPosition().get(); - if (inc > 0) { - zoomValue.set("In"); - horizontalScrollbarModel.zoomAtPosition(newPos, 0.25); - } else { - zoomValue.set("Out"); - horizontalScrollbarModel.zoomAtPosition(newPos, -0.25); - } - } else { - if (inc > 0) { - cursorTrack.selectNext(); - } else { - cursorTrack.selectPrevious(); - } - } - } - - private void handleVerticalZoom(final BasicStringValue zoomVerticalValue, final int inc) { - if (inc > 0) { - zoomVerticalValue.set("Zoom In"); - arranger.zoomInLaneHeightsSelected(); - } else { - zoomVerticalValue.set("Zoom Out"); - arranger.zoomOutLaneHeightsSelected(); - } - } - - private void handleSceneSelect(final int inc) { - if (inc > 0) { - sceneBank.scrollForwards(); - focusScene.selectInEditor(); - } else { - sceneBank.scrollBackwards(); - focusScene.selectInEditor(); - } - } - - private RgbState canPageNavigateBackward() { - if (shiftState.get()) { - return cursorDevice.hasPrevious().get() ? RgbState.DIM_WHITE : RgbState.OFF; - } - return deviceRemotes.canGoBack() ? RgbState.DIM_WHITE : RgbState.OFF; - } - - private RgbState canPageNavigateForward() { - if (shiftState.get()) { - return cursorDevice.hasNext().get() ? RgbState.DIM_WHITE : RgbState.OFF; - } - return deviceRemotes.canGoForward() ? RgbState.DIM_WHITE : RgbState.OFF; - } - - private void navigateBackward() { - if (shiftState.get()) { - cursorDevice.selectPrevious(); - } else { - deviceRemotes.selectPreviousPage(); - } - } - - private void navigateForward() { - if (shiftState.get()) { - cursorDevice.selectNext(); - } else { - deviceRemotes.selectNextPage(); - } - } - - @Override - protected void onActivate() { - super.onActivate(); - launcherLayer.setIsActive(panelLayout.get() == LayoutType.LAUNCHER); - arrangerLayer.setIsActive(panelLayout.get() == LayoutType.ARRANGER); - deviceRemotes.setActive(true); - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - launcherLayer.setIsActive(false); - arrangerLayer.setIsActive(false); - deviceRemotes.setActive(false); - } -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/LcDawControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/LcDawControlLayer.java new file mode 100644 index 00000000..44d92156 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/LcDawControlLayer.java @@ -0,0 +1,126 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.CcConstValues; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.SegmentDisplayBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchButton; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Component; + +@Component(tag = "LCModel") +public class LcDawControlLayer extends AbstractDawControlLayer { + + private final LaunchControlXlHwElements hwElements; + private SpecControl specControl; + + public LcDawControlLayer(final Layers layers, final LaunchControlXlHwElements hwElements, + final LaunchViewControl viewControl, final DisplayControl displayControl, + final TransportHandler transportHandler, final ControllerHost host) { + super(layers, hwElements, viewControl, displayControl, transportHandler, host); + this.hwElements = hwElements; + deviceRemotes.bind(this, hwElements, displayControl); + final LaunchButton lcButton = hwElements.getButtons(CcConstValues.DAW_SPEC); + lcButton.bindIsPressed(this, this::handleSpecButton); + + selectTrackBinding = + new SegmentDisplayBinding("Select Track", cursorTrack.name(), displayControl.getTemporaryDisplay()); + this.addBinding(selectTrackBinding); + + bindNavigation(hwElements); + transportHandler.setTrackNavigation(this::navigateTracks); + } + + public void setSpecOverlay(final SpecControl specControl) { + this.specControl = specControl; + } + + private void handleSpecButton(final Boolean pressed) { + this.specControl.setActive(pressed); + } + + private void bindNavigation(final LaunchControlXlHwElements hwElements) { + final LaunchButton trackLeftButton = hwElements.getButtons(CcConstValues.TRACK_LEFT); + final LaunchButton trackRightButton = hwElements.getButtons(CcConstValues.TRACK_RIGHT); + + trackLeftButton.bindLight(this, () -> viewControl.canNavLeft() ? RgbState.WHITE : RgbState.OFF); + trackRightButton.bindLight( + this, () -> viewControl.canNavRight(hwElements.getShiftState().get()) ? RgbState.WHITE : RgbState.OFF); + trackRightButton.bindRepeatHold(this, this::navTrackRight); + trackLeftButton.bindRepeatHold(this, this::navTrackLeft); + + final LaunchButton pageUpButton = hwElements.getButtons(CcConstValues.PAGE_UP); + final LaunchButton pageDownButton = hwElements.getButtons(CcConstValues.PAGE_DOWN); + pageUpButton.bindLight(this, this::canPageNavigateBackward); + pageDownButton.bindLight(this, this::canPageNavigateForward); + pageUpButton.bindRepeatHold(this, this::navigateBackward); + pageDownButton.bindRepeatHold(this, this::navigateForward); + } + + private RgbState canPageNavigateBackward() { + if (shiftState.get()) { + return cursorDevice.hasPrevious().get() ? RgbState.DIM_WHITE : RgbState.OFF; + } + return deviceRemotes.canGoBack() ? RgbState.DIM_WHITE : RgbState.OFF; + } + + private RgbState canPageNavigateForward() { + if (shiftState.get()) { + return cursorDevice.hasNext().get() ? RgbState.DIM_WHITE : RgbState.OFF; + } + return deviceRemotes.canGoForward() ? RgbState.DIM_WHITE : RgbState.OFF; + } + + private void navigateBackward() { + if (shiftState.get()) { + cursorDevice.selectPrevious(); + } else { + deviceRemotes.selectPreviousPage(); + } + displayControl.cancelTemporary(); + } + + private void navigateForward() { + if (shiftState.get()) { + cursorDevice.selectNext(); + } else { + deviceRemotes.selectNextPage(); + } + displayControl.cancelTemporary(); + } + + public void navTrackRight() { + if (shiftState.get()) { + viewControl.navigateCursorBy(8); + } else { + viewControl.navigateCursorBy(1); + } + deviceDisplayBinding.blockUpdate(); + } + + public void navTrackLeft() { + if (shiftState.get()) { + viewControl.navigateCursorBy(-8); + } else { + viewControl.navigateCursorBy(-1); + } + deviceDisplayBinding.blockUpdate(); + } + + + @Override + protected void onActivate() { + super.onActivate(); + deviceRemotes.setActive(true); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + deviceRemotes.setActive(false); + } + +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/LcMixerLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/LcMixerLayer.java new file mode 100644 index 00000000..c6e39dac --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/LcMixerLayer.java @@ -0,0 +1,296 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Send; +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; +import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.CcConstValues; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.AbsoluteEncoderBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.DisplayId; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.LightSendValueBindings; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.LightValueBindings; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.ParameterDisplayBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.RelativeEncoderBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.SegmentDisplayBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchAbsoluteEncoder; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchButton; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchRelativeEncoder; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.GradientColor; +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.di.Inject; + +@Component(tag = "LCModel") +public class LcMixerLayer extends AbstractMixerLayer { + + @Inject + private LcDawControlLayer dawLayer; + + private final Layer panLayer; + private final Layer sendLayer; + + private boolean panFocus = true; + private boolean updateFxText = false; + private String fxTextName = ""; + private SpecControl specControl; + + private enum ButtonMode { + SELECT, + MUTE, + SOLO, + ARM + } + + private ButtonMode buttonMode = ButtonMode.SELECT; + + public LcMixerLayer(final Layers layers, final LaunchControlMidiProcessor midiProcessor, final ControllerHost host, + final LaunchViewControl viewControl, final LaunchControlXlHwElements hwElements, + final DisplayControl displayControl, final TransportHandler transportHandler, final ButtonLayers buttonLayers) { + super(layers, midiProcessor, host, viewControl, hwElements, displayControl, transportHandler, buttonLayers); + panLayer = new Layer(layers, "PAN"); + sendLayer = new Layer(layers, "SEND"); + + final TrackBank trackBank = viewControl.getTrackBank(); + for (int i = 0; i < 8; i++) { + bindTrack(hwElements, trackBank, i); + } + bindNavigation(hwElements); + final LaunchButton lcButton = hwElements.getButtons(CcConstValues.DAW_SPEC); + lcButton.bindIsPressed(this, this::handleSpecButton); + } + + @Activate + public void init() { + this.setIsActive(true); + applyMode(); + } + + public void setSpecOverlay(final SpecControl specControl) { + this.specControl = specControl; + } + + private void handleSpecButton(final Boolean pressed) { + specControl.setActive(pressed); + midiProcessor.setToRelative(1, pressed); + } + + private void bindNavigation(final LaunchControlXlHwElements hwElements) { + final LaunchButton trackLeftButton = hwElements.getButtons(CcConstValues.TRACK_LEFT); + final LaunchButton trackRightButton = hwElements.getButtons(CcConstValues.TRACK_RIGHT); + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + + trackLeftButton.bindLight(mixerLayer, () -> viewControl.canNavLeft() ? RgbState.WHITE : RgbState.OFF); + trackRightButton.bindLight( + mixerLayer, () -> viewControl.canNavRight(shiftState.get()) ? RgbState.WHITE : RgbState.OFF); + + trackRightButton.bindRepeatHold(mixerLayer, this::navTrackRight); + trackLeftButton.bindRepeatHold(mixerLayer, this::navTrackLeft); + + final LaunchButton functionButton = hwElements.getButtons(CcConstValues.MUTE_SELECT_MODE); + functionButton.bindIsPressed(this, this::buttonModePressed); + functionButton.bindLight(this, this::functionButtonColor); + final SegmentDisplayBinding trackDisplayBinding = + new SegmentDisplayBinding("Select Track", cursorTrack.name(), displayControl.getFixedDisplay()); + mixerLayer.addBinding(trackDisplayBinding); + + final SendBank refBank = viewControl.getRefSendBank(); + refBank.canScrollBackwards().markInterested(); + refBank.canScrollForwards().markInterested(); + refBank.getItemAt(0).name().addValueObserver(this::updateFxSendName); + final LaunchButton pageUpButton = hwElements.getButtons(CcConstValues.PAGE_DOWN); + final LaunchButton pageDownButton = hwElements.getButtons(CcConstValues.PAGE_UP); + pageUpButton.bindLight( + mixerLayer, () -> panFocus || refBank.canScrollForwards().get() ? RgbState.WHITE : RgbState.OFF); + pageDownButton.bindLight( + mixerLayer, () -> !panFocus || refBank.canScrollBackwards().get() ? RgbState.WHITE : RgbState.OFF); + pageUpButton.bindRepeatHold(mixerLayer, () -> navigatePage(1)); + pageDownButton.bindRepeatHold(mixerLayer, () -> navigatePage(-1)); + } + + private void updateFxSendName(final String name) { + this.fxTextName = name; + if (updateFxText) { + displayControl.show2LineTemporary("Row 1 Control", "Send : %s".formatted(fxTextName)); + updateFxText = false; + } + } + + private void navigatePage(final int dir) { + if (panFocus) { + if (dir > 0) { + panFocus = false; + applyMode(); + displayControl.show2LineTemporary("Row 1 Control", "Send : %s".formatted(fxTextName)); + } else { + displayControl.show2LineTemporary("Row 1 Control", "Pan"); + } + } else { + if (dir < 0 && !viewControl.getRefSendBank().canScrollBackwards().get()) { + panFocus = true; + applyMode(); + displayControl.show2LineTemporary("Row 1 Control", "Pan"); + } else { + updateFxText = true; + viewControl.navigateSends(dir); + } + } + } + + private void bindTrack(final LaunchControlXlHwElements hwElements, final TrackBank trackBank, final int index) { + final Track track = trackBank.getItemAt(index); + final Send send1 = track.sendBank().getItemAt(0); + send1.name().markInterested(); + track.color().addValueObserver((r, g, b) -> changeTrackColor(index, ColorLookup.toColor(r, g, b))); + track.addIsSelectedInMixerObserver(select -> { + if (select) { + this.selectedTrackIndex.set(index); + } + }); + track.arm().markInterested(); + track.exists().markInterested(); + track.mute().markInterested(); + track.solo().markInterested(); + + final LaunchAbsoluteEncoder row1Encoder = hwElements.getAbsoluteEncoder(0, index); + final LaunchAbsoluteEncoder row2Encoder = hwElements.getAbsoluteEncoder(1, index); + + //fixedVolumeLabel + final ParameterDisplayBinding volumeDisplayBinding = + new ParameterDisplayBinding( + new DisplayId(row2Encoder.getTargetId(), displayControl), track.name(), + track.volume()); + mixerLayer.addBinding(volumeDisplayBinding); + mixerLayer.addBinding(new LightValueBindings(track.volume(), row2Encoder.getLight(), GradientColor.WHITE)); + mixerLayer.addBinding(new AbsoluteEncoderBinding(track.volume(), row2Encoder)); + + final ParameterDisplayBinding send1DisplayBinding = + new ParameterDisplayBinding(new DisplayId(row1Encoder.getTargetId(), displayControl), track.name(), send1); + sendLayer.addBinding(send1DisplayBinding); + sendLayer.addBinding(new LightSendValueBindings(send1, row1Encoder.getLight())); + sendLayer.addBinding(new AbsoluteEncoderBinding(send1, row1Encoder)); + + final LaunchRelativeEncoder relativeRow2Encoder = hwElements.getRelativeEncoder(0, index); + final ParameterDisplayBinding panDisplayBinding = + new ParameterDisplayBinding( + new DisplayId(relativeRow2Encoder.getTargetId(), displayControl), track.name(), track.pan()); + panLayer.addBinding(panDisplayBinding); + panLayer.addBinding( + new LightValueBindings(track.pan(), hwElements.getRelativeEncoder(0, index).getLight(), GradientColor.PAN)); + panLayer.addBinding(new RelativeEncoderBinding(track.pan(), hwElements.getRelativeEncoder(0, index))); + + final LaunchButton button = hwElements.getRowButtons(0, index); + button.bindLight(this.buttonLayers.getSelectLayer(), () -> selectColor(track, index)); + button.bindPressed(this.buttonLayers.getSelectLayer(), () -> selectTrack(track)); + button.bindLight(this.buttonLayers.getSoloLayer(), () -> soloColor(track)); + button.bindIsPressed(this.buttonLayers.getSoloLayer(), pressed -> toggleSolo(pressed, track)); + button.bindLight(this.buttonLayers.getArmLayer(), () -> armColor(track)); + button.bindIsPressed(this.buttonLayers.getArmLayer(), pressed -> toggleArm(pressed, track)); + button.bindLight(this.buttonLayers.getMuteLayer(), () -> muteColor(track)); + button.bindPressed(this.buttonLayers.getMuteLayer(), () -> track.mute().toggle()); + transportHandler.assignTransportButtons(hwElements.getButtons(1), this.buttonLayers.getMuteLayer()); + } + + private void buttonModePressed(final boolean pressed) { + if (!pressed) { + return; + } + switch (this.buttonMode) { + case SELECT -> { + buttonMode = ButtonMode.SOLO; + displayControl.show2LineTemporary("Button Function", "Solo"); + } + case SOLO -> { + buttonMode = ButtonMode.MUTE; + displayControl.show2LineTemporary("Button Function", "Mute"); + } + case MUTE -> { + buttonMode = ButtonMode.ARM; + displayControl.show2LineTemporary("Button Function", "Arm"); + } + case ARM -> { + buttonMode = ButtonMode.SELECT; + displayControl.show2LineTemporary("Button Function", "Select"); + } + } + applyButtonMode(); + } + + private RgbState functionButtonColor() { + return switch (this.buttonMode) { + case SELECT -> RgbState.WHITE; + case SOLO -> RgbState.YELLOW; + case ARM -> RgbState.RED; + case MUTE -> RgbState.ORANGE; + }; + } + + @Override + protected void applyMode() { + if (mode == BaseMode.MIXER) { + midiProcessor.setToRelative(0, panFocus); + midiProcessor.setToRelative(1, false); + } else { + midiProcessor.setToRelative(0, true); + midiProcessor.setToRelative(1, true); + } + + this.mixerLayer.setIsActive(mode == BaseMode.MIXER); + this.dawLayer.setIsActive(mode == BaseMode.DAW); + if (mode == BaseMode.MIXER) { + this.panLayer.setIsActive(panFocus); + this.sendLayer.setIsActive(!panFocus); + } else { + this.panLayer.setIsActive(false); + this.sendLayer.setIsActive(false); + } + applyButtonMode(); + } + + private void applyButtonMode() { + this.buttonLayers.getSelectLayer().setIsActive(buttonMode == ButtonMode.SELECT); + this.buttonLayers.getSoloLayer().setIsActive(buttonMode == ButtonMode.SOLO); + this.buttonLayers.getMuteLayer().setIsActive(buttonMode == ButtonMode.MUTE); + this.buttonLayers.getArmLayer().setIsActive(buttonMode == ButtonMode.ARM); + } + + private void activateButtonModes(final boolean active) { + if (active) { + applyButtonMode(); + } else { + this.buttonLayers.getSelectLayer().setIsActive(active); + this.buttonLayers.getMuteLayer().setIsActive(active); + this.buttonLayers.getSoloLayer().setIsActive(active); + this.buttonLayers.getArmLayer().setIsActive(active); + } + } + + public void navTrackRight() { + if (shiftState.get()) { + viewControl.navigateCursorBy(8); + } else { + viewControl.navigateCursorBy(1); + } + displayControl.cancelTemporary(); + } + + public void navTrackLeft() { + if (shiftState.get()) { + viewControl.navigateCursorBy(-8); + } else { + viewControl.navigateCursorBy(-1); + } + displayControl.cancelTemporary(); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/MixerLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/MixerLayer.java deleted file mode 100644 index cb3e1131..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/MixerLayer.java +++ /dev/null @@ -1,319 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; - -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; -import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.CcConstValues; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.*; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchAbsoluteEncoder; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchButton; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchRelativeEncoder; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; -import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.GradientColor; -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.di.Inject; -import com.bitwig.extensions.framework.values.BasicIntegerValue; -import com.bitwig.extensions.framework.values.BasicStringValue; -import com.bitwig.extensions.framework.values.BooleanValueObject; - -@Component -public class MixerLayer extends Layer { - - @Inject - private DawControlLayer dawLayer; - - private final Layer mixerLayer; - private final Layer selectLayer; - private final Layer soloLayer; - private final Layer armLayer; - private final Layer muteLayer; - - private BaseMode mode = BaseMode.MIXER; - private final RgbState[] trackColors = new RgbState[8]; - private final BasicIntegerValue selectedTrackIndex = new BasicIntegerValue(); - private Row1ButtonMode row1Mode = Row1ButtonMode.SOLO; - private Row2ButtonMode row2Mode = Row2ButtonMode.SELECT; - private final DisplayControl displayControl; - private int armHeld = 0; - private int soloHeld = 0; - private final Project project; - private final BooleanValueObject shiftState; - private final LaunchViewControl viewControl; - private final TransportHandler transportHandler; - - private enum Row1ButtonMode { - SOLO, - ARM - } - - private enum Row2ButtonMode { - SELECT, - MUTE - } - - public MixerLayer(final Layers layers, final LaunchControlXlHwElements hwElements, - final LaunchViewControl viewControl, final DisplayControl displayControl, - final LaunchControlMidiProcessor midiProcessor, final ControllerHost host, - final TransportHandler transportHandler) { - super(layers, "MAIN"); - this.project = host.getProject(); - this.viewControl = viewControl; - project.hasArmedTracks().markInterested(); - project.hasSoloedTracks().markInterested(); - this.displayControl = displayControl; - this.transportHandler = transportHandler; - this.shiftState = hwElements.getShiftState(); - midiProcessor.addModeListener(this::handleModeChange); - mixerLayer = new Layer(layers, "MIXER_LAYER"); - selectLayer = new Layer(layers, "SELECT"); - soloLayer = new Layer(layers, "SOLO"); - armLayer = new Layer(layers, "ARM"); - muteLayer = new Layer(layers, "MUTE"); - - final BasicStringValue fixedVolumeLabel = new BasicStringValue("Volume"); - final BasicStringValue fixedPanLabel = new BasicStringValue("Panning"); - - final TrackBank trackBank = viewControl.getTrackBank(); - for (int i = 0; i < 8; i++) { - bindTrack(hwElements, trackBank, i, fixedPanLabel, fixedVolumeLabel); - } - - bindNavigation(hwElements); - transportHandler.bindTransport(this); - final LaunchButton soloArmButton = hwElements.getButton(CcConstValues.SOLO_ARM_MODE); - final LaunchButton muteSelectButton = hwElements.getButton(CcConstValues.MUTE_SELECT_MODE); - - final LaunchButton specModeButton = hwElements.getButton(CcConstValues.DAW_SPEC); - specModeButton.bindLight(this, () -> RgbState.BLUE); - //specModeButton.bindPressed(this, () -> LaunchControlXlMk3Extension.println(" > PRESS SPEC >")); - - soloArmButton.bindLight(this, () -> row1Mode == Row1ButtonMode.ARM ? RgbState.RED : RgbState.YELLOW); - muteSelectButton.bindLight(this, () -> row2Mode == Row2ButtonMode.SELECT ? RgbState.WHITE : RgbState.ORANGE); - soloArmButton.bindPressed(this, this::toggleSoloArmMode); - muteSelectButton.bindPressed(this, this::toggleSelectMuteMode); - } - - private void bindNavigation(final LaunchControlXlHwElements hwElements) { - final LaunchButton pageUpButton = hwElements.getButton(CcConstValues.PAGE_UP); - final LaunchButton pageDownButton = hwElements.getButton(CcConstValues.PAGE_DOWN); - final CursorTrack cursorTrack = viewControl.getCursorTrack(); - transportHandler.bindTrackNavigation(mixerLayer); - - mixerLayer.addBinding( - new SegmentDisplayBinding("Select Track", cursorTrack.name(), displayControl.getFixedDisplay())); - - final SendBank refBank = viewControl.getRefSendBank(); - final Send send1 = refBank.getItemAt(0); - final Send send2 = refBank.getItemAt(1); - send1.name().markInterested(); - send2.name().markInterested(); - refBank.canScrollBackwards().markInterested(); - refBank.canScrollForwards().markInterested(); - - pageUpButton.bindLight(mixerLayer, () -> refBank.canScrollBackwards().get() ? RgbState.WHITE : RgbState.OFF); - pageDownButton.bindLight(mixerLayer, () -> refBank.canScrollForwards().get() ? RgbState.WHITE : RgbState.OFF); - pageUpButton.bindRepeatHold(mixerLayer, () -> viewControl.navigateSends(-1)); - pageDownButton.bindRepeatHold(mixerLayer, () -> viewControl.navigateSends(1)); - refBank.scrollPosition().addValueObserver( - pos -> displayControl.show2Line("Sends", "%s - %s".formatted(send1.name().get(), send2.name().get()))); - } - - - private void bindTrack(final LaunchControlXlHwElements hwElements, final TrackBank trackBank, final int index, - final BasicStringValue fixedPanLabel, final BasicStringValue fixedVolumeLabel) { - final Track track = trackBank.getItemAt(index); - final Send send1 = track.sendBank().getItemAt(0); - final Send send2 = track.sendBank().getItemAt(1); - track.color().addValueObserver((r, g, b) -> changeTrackColor(index, ColorLookup.toColor(r, g, b))); - track.addIsSelectedInMixerObserver(select -> { - if (select) { - this.selectedTrackIndex.set(index); - } - }); - track.arm().markInterested(); - track.exists().markInterested(); - track.mute().markInterested(); - track.solo().markInterested(); - - final LaunchAbsoluteEncoder row1Encoder = hwElements.getAbsoluteEncoder(0, index); - final LaunchAbsoluteEncoder row2Encoder = hwElements.getAbsoluteEncoder(1, index); - final LaunchRelativeEncoder row3Encoder = hwElements.getRelativeEncoder(2, index); - - mixerLayer.addBinding( - new AbsoluteEncoderBinding(send1, row1Encoder, displayControl, track.name(), send1.name())); - mixerLayer.addBinding(new LightSendValueBindings(send1, row1Encoder.getLight())); - mixerLayer.addBinding(new LightSendValueBindings(send2, row2Encoder.getLight())); - mixerLayer.addBinding(new LightValueBindings(track.pan(), row3Encoder.getLight(), GradientColor.PAN)); - mixerLayer.addBinding( - new AbsoluteEncoderBinding( - send2, hwElements.getAbsoluteEncoder(1, index), displayControl, track.name(), - send2.name())); - mixerLayer.addBinding( - new RelativeEncoderBinding(track.pan(), row3Encoder, displayControl, track.name(), fixedPanLabel)); - this.addBinding( - new SliderBinding( - index, track.volume(), hwElements.getSlider(index), displayControl, track.name(), - fixedVolumeLabel)); - final LaunchButton row2Button = hwElements.getRowButtons(1, index); - final LaunchButton row1Button = hwElements.getRowButtons(0, index); - row1Button.bindLight(armLayer, () -> armColor(track)); - row1Button.bindIsPressed(armLayer, pressed -> toggleArm(pressed, track)); - row1Button.bindLight(soloLayer, () -> soloColor(track)); - row1Button.bindIsPressed(soloLayer, pressed -> toggleSolo(pressed, track)); - - row2Button.bindLight(selectLayer, () -> selectColor(track, index)); - row2Button.bindPressed(selectLayer, () -> selectTrack(track)); - row2Button.bindLight(muteLayer, () -> muteColor(track)); - row2Button.bindPressed(muteLayer, () -> track.mute().toggle()); - } - - private void toggleSoloArmMode() { - if (this.row1Mode == Row1ButtonMode.ARM) { - this.row1Mode = Row1ButtonMode.SOLO; - displayControl.show2Line("Arm/Solo", "Solo"); - } else { - this.row1Mode = Row1ButtonMode.ARM; - displayControl.show2Line("Arm/Solo", "Arm"); - } - applyArmSoloMode(); - } - - private void toggleSelectMuteMode() { - if (this.row2Mode == Row2ButtonMode.SELECT) { - this.row2Mode = Row2ButtonMode.MUTE; - displayControl.show2Line("Mute/Select", "Mute"); - } else { - this.row2Mode = Row2ButtonMode.SELECT; - displayControl.show2Line("Mute/Select", "Select"); - } - applySelectMuteMode(); - } - - private void changeTrackColor(final int index, final int color) { - if (color == 1) { - trackColors[index] = RgbState.of(color); - } else { - trackColors[index] = RgbState.of(color).dim(); - } - } - - private void selectTrack(final Track track) { - track.selectInMixer(); - } - - private void toggleArm(final boolean pressed, final Track track) { - if (shiftState.get()) { - if (pressed) { - track.arm().toggle(); - } - } else if (pressed) { - armHeld++; - if (armHeld == 1) { - final boolean armed = track.arm().get(); - project.unarmAll(); - if (!armed) { - track.arm().toggle(); - } - } else { - track.arm().toggle(); - } - } else { - if (armHeld > 0) { - armHeld--; - } - } - } - - private void toggleSolo(final boolean pressed, final Track track) { - if (shiftState.get()) { - if (pressed) { - track.solo().toggle(); - } - } else if (pressed) { - soloHeld++; - if (soloHeld == 1) { - track.solo().toggle(true); - } else { - track.solo().toggle(); - } - } else { - if (soloHeld > 0) { - soloHeld--; - } - } - } - - private RgbState muteColor(final Track track) { - if (track.exists().get()) { - return track.mute().get() ? RgbState.ORANGE : RgbState.ORANGE_LO; - } - return RgbState.OFF; - } - - private RgbState selectColor(final Track track, final int index) { - if (track.exists().get()) { - return index == selectedTrackIndex.get() ? RgbState.WHITE : trackColors[index]; - } - return RgbState.OFF; - } - - private RgbState armColor(final Track track) { - if (track.exists().get()) { - return track.arm().get() ? RgbState.RED : RgbState.RED_LO; - } - return RgbState.OFF; - } - - private RgbState soloColor(final Track track) { - if (track.exists().get()) { - return track.solo().get() ? RgbState.YELLOW : RgbState.YELLOW_LO; - } - return RgbState.OFF; - } - - - private void handleModeChange(final BaseMode baseMode) { - this.mode = baseMode; - applyMode(); - } - - @Activate - public void init() { - this.setIsActive(true); - applyMode(); - } - - private void applyMode() { - this.mixerLayer.setIsActive(mode == BaseMode.MIXER); - this.dawLayer.setIsActive(mode == BaseMode.DAW); - applySelectMuteMode(); - applyArmSoloMode(); - selectLayer.setIsActive(true); - } - - private void applyArmSoloMode() { - this.armLayer.setIsActive(row1Mode == Row1ButtonMode.ARM); - this.soloLayer.setIsActive(row1Mode == Row1ButtonMode.SOLO); - } - - private void applySelectMuteMode() { - this.selectLayer.setIsActive(row2Mode == Row2ButtonMode.SELECT); - this.muteLayer.setIsActive(row2Mode == Row2ButtonMode.MUTE); - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - } - - @Override - protected void onActivate() { - super.onActivate(); - } -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/Remotes.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/Remotes.java index 683866a8..56389f01 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/Remotes.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/Remotes.java @@ -8,7 +8,9 @@ import com.bitwig.extension.controller.api.Parameter; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.DisableBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.DisplayId; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.LightValueBindings; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.ParameterDisplayBinding; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.RelativeEncoderBinding; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchRelativeEncoder; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; @@ -54,18 +56,24 @@ public void bind(final Layer layer, final LaunchControlXlHwElements hwElements, final LaunchRelativeEncoder row1Encoder = hwElements.getRelativeEncoder(0, i); final LaunchRelativeEncoder row2Encoder = hwElements.getRelativeEncoder(1, i); final Parameter parameter = this.getParameter(i); - layer.addBinding( - new RelativeEncoderBinding(parameter, row1Encoder, displayControl, deviceName, parameter.name())); + final ParameterDisplayBinding parameterRow1DisplayBinding = + new ParameterDisplayBinding( + new DisplayId(row1Encoder.getTargetId(), displayControl), deviceName, parameter); + layer.addBinding(parameterRow1DisplayBinding); + layer.addBinding(new RelativeEncoderBinding(parameter, row1Encoder)); layer.addBinding(new LightValueBindings(parameter, row1Encoder.getLight(), DEVICE_COLORS.get(i))); final Parameter parameter2 = getParameter2(i); - final RelativeEncoderBinding row2Binding = - new RelativeEncoderBinding(parameter2, row2Encoder, displayControl, deviceName, parameter2.name()); + final ParameterDisplayBinding parameterRow2DisplayBinding = + new ParameterDisplayBinding( + new DisplayId(row2Encoder.getTargetId(), displayControl), deviceName, parameter2); + layer.addBinding(parameterRow2DisplayBinding); + final RelativeEncoderBinding row2Binding = new RelativeEncoderBinding(parameter2, row2Encoder); layer.addBinding(row2Binding); final LightValueBindings row2LightBinding = new LightValueBindings(parameter2, row2Encoder.getLight(), DEVICE_COLORS.get(i)); layer.addBinding(row2LightBinding); - disableBindings.add(row2Binding); + disableBindings.add(parameterRow2DisplayBinding); disableBindings.add(row2LightBinding); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/SpecControl.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/SpecControl.java new file mode 100644 index 00000000..db623d34 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/SpecControl.java @@ -0,0 +1,47 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; + +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.LayoutType; + +public class SpecControl { + + private final Layer specLayer; + private final Layer specLauncherLayer; + private final TransportHandler transportHandler; + private final DisplayControl displayControl; + + public SpecControl(final Layers layers, final LaunchControlXlHwElements hwElements, + final TransportHandler transportHandler, final DisplayControl displayControl) { + + specLayer = new Layer(layers, "DAW_SPEC"); + specLauncherLayer = new Layer(layers, "DAW_SPEC_LAUNCHER"); + + this.transportHandler = transportHandler; + this.displayControl = displayControl; + transportHandler.bindControl(specLayer, hwElements, 1); + transportHandler.bindArrangerLayoutControl(specLayer, hwElements, 1); + transportHandler.bindLauncherLayoutControl(specLauncherLayer, hwElements, 1); + transportHandler.getPanelLayout().addValueObserver(this::handlePanelLayoutUpdate); + transportHandler.assignTransportButtons(hwElements.getButtons(0), specLayer); + } + + protected void handlePanelLayoutUpdate(final LayoutType newValue) { + if (specLayer.isActive()) { + specLauncherLayer.setIsActive(specLayer.isActive() && newValue == LayoutType.LAUNCHER); + } + } + + public void setActive(final boolean active) { + specLayer.setIsActive(active); + if (active) { + displayControl.show2LineTemporary("Button Row 2", "Transport Control"); + specLauncherLayer.setIsActive(transportHandler.getPanelLayout().get() == LayoutType.LAUNCHER); + } else { + specLauncherLayer.setIsActive(false); + displayControl.cancelTemporary(); + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/TransportHandler.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/TransportHandler.java index 9ae8fcae..04cc4395 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/TransportHandler.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/TransportHandler.java @@ -1,8 +1,20 @@ package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; +import java.util.List; +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.Arranger; +import com.bitwig.extension.controller.api.BeatTimeFormatter; import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CueMarker; +import com.bitwig.extension.controller.api.CueMarkerBank; import com.bitwig.extension.controller.api.CursorTrack; import com.bitwig.extension.controller.api.DocumentState; +import com.bitwig.extension.controller.api.Scene; +import com.bitwig.extension.controller.api.SceneBank; +import com.bitwig.extension.controller.api.ScrollbarModel; +import com.bitwig.extension.controller.api.SettableBeatTimeValue; import com.bitwig.extension.controller.api.SettableEnumValue; import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extension.controller.api.Transport; @@ -10,12 +22,21 @@ import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.CcConstValues; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.BooleanLightValueBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.FixedLightValueBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.RelativeDisplayControl; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchButton; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchRelativeEncoder; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.RgbColor; import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.RgbColorState; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.values.BasicStringValue; import com.bitwig.extensions.framework.values.BooleanValueObject; import com.bitwig.extensions.framework.values.FocusMode; +import com.bitwig.extensions.framework.values.LayoutType; +import com.bitwig.extensions.framework.values.ValueObject; @Component public class TransportHandler { @@ -26,16 +47,48 @@ public class TransportHandler { private final Transport transport; private final LaunchControlXlHwElements hwElements; private final LaunchViewControl viewControl; + private final DisplayControl displayControl; + private final Application application; + private final Arranger arranger; + private IntConsumer trackNavigator; + + protected double scrubDistance; + protected final ScrollbarModel horizontalScrollbarModel; + private boolean markerPositionChangePending = false; + private final SceneBank sceneBank; + protected final Scene focusScene; + + protected final ValueObject panelLayout = new ValueObject<>(LayoutType.ARRANGER); + private final BeatTimeFormatter formatter; + protected BasicStringValue zoomValue = new BasicStringValue(""); + protected BasicStringValue zoomVerticalValue = new BasicStringValue(""); + protected BasicStringValue cueMarkerValue = new BasicStringValue(""); + protected CueMarkerBank cueMarkerBank; + public TransportHandler(final ControllerHost host, final LaunchControlXlHwElements hwElements, - final LaunchViewControl viewControl, final Transport transport) { + final LaunchViewControl viewControl, final Transport transport, final DisplayControl displayControl, + final Application application) { this.transport = transport; this.shiftState = hwElements.getShiftState(); this.viewControl = viewControl; this.cursorTrack = viewControl.getCursorTrack(); + this.application = application; this.hwElements = hwElements; cursorTrack.position().markInterested(); cursorTrack.channelIndex().markInterested(); + this.displayControl = displayControl; + this.arranger = host.createArranger(); + + this.formatter = host.createBeatTimeFormatter(":", 2, 1, 1, 0); + horizontalScrollbarModel = this.arranger.getHorizontalScrollbarModel(); + horizontalScrollbarModel.getContentPerPixel().addValueObserver(this::handleZoomLevel); + cueMarkerBank = arranger.createCueMarkerBank(1); + + sceneBank = viewControl.getTrackBank().sceneBank(); + focusScene = sceneBank.getScene(0); + focusScene.name().markInterested(); + sceneBank.setIndication(true); final TrackBank trackBank = viewControl.getTrackBank(); trackBank.itemCount().addValueObserver(count -> { @@ -47,6 +100,8 @@ public TransportHandler(final ControllerHost host, final LaunchControlXlHwElemen new String[] {FocusMode.LAUNCHER.getDescriptor(), FocusMode.ARRANGER.getDescriptor()}, FocusMode.ARRANGER.getDescriptor()); focusMode.addValueObserver(mode -> this.focusMode = FocusMode.toMode(mode)); + configureZoomAndMarkers(); + application.panelLayout().addValueObserver(layout -> this.panelLayout.set(LayoutType.toType(layout))); } public Transport getTransport() { @@ -54,8 +109,8 @@ public Transport getTransport() { } public void bindTransport(final Layer layer) { - final LaunchButton playButton = hwElements.getButton(CcConstValues.PLAY); - final LaunchButton recButton = hwElements.getButton(CcConstValues.RECORD); + final LaunchButton playButton = hwElements.getButtons(CcConstValues.PLAY); + final LaunchButton recButton = hwElements.getButtons(CcConstValues.RECORD); transport.isPlaying().markInterested(); transport.isClipLauncherOverdubEnabled().markInterested(); transport.isArrangerRecordEnabled().markInterested(); @@ -65,9 +120,97 @@ public void bindTransport(final Layer layer) { recButton.bindPressed(layer, this::handleRecordPressed); } + public void bindControl(final Layer specLayer, final LaunchControlXlHwElements hwElements, final int rowIndex) { + final LaunchRelativeEncoder playbackPosEncoder = hwElements.getRelativeEncoder(rowIndex, 0); + final LaunchRelativeEncoder loopStartEncoder = hwElements.getRelativeEncoder(rowIndex, 3); + final LaunchRelativeEncoder looEndEncoder = hwElements.getRelativeEncoder(rowIndex, 4); + final SettableBeatTimeValue playPosition = transport.getPosition(); + bindPosition(specLayer, playPosition, playbackPosEncoder, "PlaybackPosition", false, true, RgbColor.BLUE_LOW); + + bindPosition( + specLayer, transport.arrangerLoopStart(), loopStartEncoder, "Loop Start", true, false, RgbColor.BLUE_LOW); + bindPosition( + specLayer, transport.arrangerLoopDuration(), looEndEncoder, "Loop Duration", true, false, + RgbColor.BLUE_LOW); + + final BasicStringValue loopActive = new BasicStringValue("On"); + transport.isArrangerLoopEnabled().addValueObserver(active -> loopActive.set(active ? "ON" : "OFF")); + final LaunchRelativeEncoder loopActiveEncoder = hwElements.getRelativeEncoder(rowIndex, 5); + final RelativeDisplayControl loopControl = + new RelativeDisplayControl( + loopActiveEncoder.getTargetId(), displayControl, "Transport", "Loop", loopActive, + inc -> this.handleLoopOnOff(transport, inc)); + loopActiveEncoder.bindIncrementAction(specLayer, loopControl::handleInc); + specLayer.addBinding(loopControl); + specLayer.addBinding( + new BooleanLightValueBinding( + loopActiveEncoder.getLight(), transport.isArrangerLoopEnabled(), + RgbColor.BLUE_LOW, RgbColor.BLUE_DIM)); + + final LaunchRelativeEncoder markerSelectionEncoder = hwElements.getRelativeEncoder(rowIndex, 6); + specLayer.addBinding(new FixedLightValueBinding(markerSelectionEncoder.getLight(), RgbColor.YELLOW)); + final RelativeDisplayControl cueMarkerControl = new RelativeDisplayControl( + markerSelectionEncoder.getTargetId(), displayControl, "Transport", "Marker Select", cueMarkerValue, + this::handleCuePointSelection, + () -> markerSelectionEncoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 48)); + markerSelectionEncoder.bindIncrementAction(specLayer, cueMarkerControl::handleInc); + specLayer.addBinding(cueMarkerControl); + + final LaunchRelativeEncoder tempoEncoder = hwElements.getRelativeEncoder(rowIndex, 7); + specLayer.addBinding(new FixedLightValueBinding(tempoEncoder.getLight(), RgbColor.BLUE_LOW)); + final RelativeDisplayControl tempoControl = + new RelativeDisplayControl( + tempoEncoder.getTargetId(), displayControl, "Transport", "Tempo", transport.tempo().displayedValue(), + inc -> transport.tempo().incRaw(inc)); + tempoEncoder.bindIncrementAction(specLayer, tempoControl::handleInc); + specLayer.addBinding(tempoControl); + } + + public void bindArrangerLayoutControl(final Layer layer, final LaunchControlXlHwElements hwElements, + final int rowIndex) { + final LaunchRelativeEncoder zoomHorizontalEncoder = hwElements.getRelativeEncoder(rowIndex, 1); + final LaunchRelativeEncoder zoomVerticalEncoder = hwElements.getRelativeEncoder(rowIndex, 2); + + layer.addBinding(new FixedLightValueBinding(zoomHorizontalEncoder.getLight(), RgbColor.LOW_WHITE)); + final RelativeDisplayControl zoomArrangerControl = getHorizontalZoomControl(zoomHorizontalEncoder); + zoomHorizontalEncoder.bindIncrementAction(layer, zoomArrangerControl::handleInc); + layer.addBinding(zoomArrangerControl); + + layer.addBinding(new FixedLightValueBinding(zoomVerticalEncoder.getLight(), RgbColor.LOW_WHITE)); + setUpVerticalZoomEncoder(layer, zoomVerticalEncoder); + } + + public void bindLauncherLayoutControl(final Layer layer, final LaunchControlXlHwElements hwElements, + final int rowIndex) { + final LaunchRelativeEncoder zoomHorizontalEncoder = hwElements.getRelativeEncoder(rowIndex, 1); + final LaunchRelativeEncoder zoomVerticalEncoder = hwElements.getRelativeEncoder(rowIndex, 2); + + layer.addBinding(new FixedLightValueBinding(zoomHorizontalEncoder.getLight(), RgbColor.ORANGE)); + final RelativeDisplayControl trackScrollView = + new RelativeDisplayControl( + zoomHorizontalEncoder.getTargetId(), displayControl, "Transport", "Select Track", cursorTrack.name(), + this::navigateTracks); + zoomHorizontalEncoder.bindIncrementAction(layer, trackScrollView::handleInc); + layer.addBinding(trackScrollView); + + // This is scene select + layer.addBinding(new FixedLightValueBinding(zoomVerticalEncoder.getLight(), RgbColor.BLUE_LOW)); + setUpSceneEncoder(layer, zoomVerticalEncoder); + } + + public void setTrackNavigation(final IntConsumer trackNavigator) { + this.trackNavigator = trackNavigator; + } + + private void navigateTracks(final int dir) { + if (this.trackNavigator != null) { + this.trackNavigator.accept(dir); + } + } + public void bindTrackNavigation(final Layer layer) { - final LaunchButton trackLeftButton = hwElements.getButton(CcConstValues.TRACK_LEFT); - final LaunchButton trackRightButton = hwElements.getButton(CcConstValues.TRACK_RIGHT); + final LaunchButton trackLeftButton = hwElements.getButtons(CcConstValues.TRACK_LEFT); + final LaunchButton trackRightButton = hwElements.getButtons(CcConstValues.TRACK_RIGHT); trackLeftButton.bindLight(layer, () -> canNavLeft(cursorTrack) ? RgbState.WHITE : RgbState.OFF); trackRightButton.bindLight(layer, () -> canNavRight(cursorTrack) ? RgbState.WHITE : RgbState.OFF); @@ -132,4 +275,272 @@ public void navLeft() { } } + public ValueObject getPanelLayout() { + return panelLayout; + } + + private void configureZoomAndMarkers() { + this.panelLayout.addValueObserver(this::handlePanelLayoutUpdate); + cursorTrack.name().addValueObserver(this::handleCursorTrackNameUpdate); + final CueMarker marker = cueMarkerBank.getItemAt(0); + marker.position().markInterested(); + marker.exists().addValueObserver(exists -> updateCueMarker(cueMarkerValue, marker.name().get(), exists)); + marker.name().addValueObserver(name -> updateCueMarker(cueMarkerValue, name, marker.exists().get())); + marker.position().addValueObserver(this::updateMarkerPosition); + } + + protected void handlePanelLayoutUpdate(final LayoutType newValue) { + if (newValue == LayoutType.LAUNCHER) { + zoomValue.set(cursorTrack.name().get()); + zoomVerticalValue.set(focusScene.name().get()); + } + } + + protected void bindPosition(final Layer layer, final SettableBeatTimeValue position, + final LaunchRelativeEncoder encoder, final String label, final boolean hasMinimum, final boolean deceleration, + final RgbColor color) { + + final BasicStringValue transportPosition = new BasicStringValue(""); + position.addValueObserver(value -> transportPosition.set(position.getFormatted(formatter))); + + final IntConsumer valueModifier = hasMinimum ? inc -> incPositionBounded(position, inc * 4.0) : inc -> { + handlePositionIncrementWithFocus(position, inc); + }; + final RelativeDisplayControl positionControl = + new RelativeDisplayControl( + encoder.getTargetId(), displayControl, "Transport", label, transportPosition, + valueModifier); + + encoder.bindIncrementAction(layer, positionControl::handleInc); + layer.addBinding(positionControl); + layer.addBinding(new FixedLightValueBinding(encoder.getLight(), color)); + } + + private void handleZoomLevel(final double v) { + if (v <= 0) { + return; + } + this.scrubDistance = roundToNearestPowerOfTwo(80 * v); + } + + public static double roundToNearestPowerOfTwo(final double value) { + if (value <= 0) { + throw new IllegalArgumentException("Value must be greater than zero."); + } + final double log2 = Math.log(value) / Math.log(2); + final double roundedPower = Math.round(log2); + return Math.pow(2, roundedPower); + } + + + private void handlePositionIncrementWithFocus(final SettableBeatTimeValue position, final int inc) { + final double newPos = position.get() + (inc * scrubDistance); + horizontalScrollbarModel.zoomAtPosition(newPos, 0); + position.set(newPos); + } + + private void incPositionBounded(final SettableBeatTimeValue position, final double inc) { + final double newValue = position.get() + inc; + if (newValue >= 0.0) { + position.set(newValue); + } else { + position.set(0.0); + } + } + + private void updateCueMarker(final BasicStringValue cueMarkerValue, final String name, final boolean exists) { + if (exists) { + cueMarkerValue.set("Marker: %s".formatted(name)); + } else { + cueMarkerValue.set("No Markers"); + } + } + + protected void handleLoopOnOff(final Transport transport, final int inc) { + transport.isArrangerLoopEnabled().set(inc > 0); + } + + protected void handleCuePointSelection(final int inc) { + if (inc < 0) { + cueMarkerBank.scrollBackwards(); + } else { + cueMarkerBank.scrollForwards(); + } + markerPositionChangePending = true; + } + + protected void handleHorizontalZoom(final BasicStringValue zoomValue, final int inc) { + if (this.panelLayout.get() == LayoutType.ARRANGER) { + final double newPos = transport.getPosition().get(); + if (inc > 0) { + zoomValue.set("In"); + horizontalScrollbarModel.zoomAtPosition(newPos, 0.25); + } else { + zoomValue.set("Out"); + horizontalScrollbarModel.zoomAtPosition(newPos, -0.25); + } + } else { + if (inc > 0) { + cursorTrack.selectNext(); + } else { + cursorTrack.selectPrevious(); + } + } + } + + protected void handleVerticalZoom(final BasicStringValue zoomVerticalValue, final int inc) { + if (inc > 0) { + zoomVerticalValue.set("Zoom In"); + arranger.zoomInLaneHeightsSelected(); + } else { + zoomVerticalValue.set("Zoom Out"); + arranger.zoomOutLaneHeightsSelected(); + } + } + + private void updateMarkerPosition(final double pos) { + if (markerPositionChangePending) { + markerPositionChangePending = false; + transport.getPosition().set(pos); + } + } + + private void handleCursorTrackNameUpdate(final String name) { + if (this.panelLayout.get() != LayoutType.ARRANGER) { + zoomValue.set(name); + } + } + + protected RelativeDisplayControl getHorizontalZoomControl(final LaunchRelativeEncoder encoder) { + final IncrementDecelerator horizontalZoomIncrementor = + new IncrementDecelerator(inc -> handleHorizontalZoom(zoomValue, inc), 50); + final RelativeDisplayControl zoomArrangerControl = + new RelativeDisplayControl( + encoder.getTargetId(), displayControl, "Transport", "Zoom Arranger", zoomValue, + horizontalZoomIncrementor, + () -> encoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 64)); + return zoomArrangerControl; + } + + protected void setUpVerticalZoomEncoder(final Layer layer, final LaunchRelativeEncoder encoder) { + final IncrementDecelerator verticalZoomIncrementor = + new IncrementDecelerator(inc -> handleVerticalZoom(zoomVerticalValue, inc), 60); + final RelativeDisplayControl zoomVerticalControl = new RelativeDisplayControl( + encoder.getTargetId(), displayControl, "Transport", "Zoom Tracks", zoomVerticalValue, + verticalZoomIncrementor, + () -> encoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 32)); + encoder.bindIncrementAction(layer, zoomVerticalControl::handleInc); + layer.addBinding(zoomVerticalControl); + } + + protected void setUpSceneEncoder(final Layer layer, final LaunchRelativeEncoder encoder) { + final IncrementDecelerator verticalZoomIncrementor = new IncrementDecelerator(this::handleSceneSelect, 60); + final RelativeDisplayControl zoomVerticalControl = new RelativeDisplayControl( + encoder.getTargetId(), displayControl, "Transport", "Scene Select", focusScene.name(), + verticalZoomIncrementor, + () -> encoder.setEncoderBehavior(LaunchRelativeEncoder.EncoderMode.ACCELERATED, 32)); + encoder.bindIncrementAction(layer, zoomVerticalControl::handleInc); + layer.addBinding(zoomVerticalControl); + } + + private void handleSceneSelect(final int inc) { + if (inc > 0) { + sceneBank.scrollForwards(); + focusScene.selectInEditor(); + } else { + sceneBank.scrollBackwards(); + focusScene.selectInEditor(); + } + } + + public void assignTransportButtons(final List buttons, final Layer layer) { + final LaunchButton playButton = buttons.get(0); + transport.isPlaying().markInterested(); + playButton.bindLight(layer, () -> transport.isPlaying().get() ? RgbState.GREEN : RgbState.DIM_GREEN); + playButton.bindPressed(layer, this::handlePlay); + + final LaunchButton stopButton = buttons.get(1); + stopButton.bindLight(layer, () -> transport.isPlaying().get() ? RgbState.WHITE : RgbState.DIM_WHITE); + stopButton.bindPressed(layer, this::handleStop); + + final LaunchButton recButton = buttons.get(2); + transport.isArrangerRecordEnabled().markInterested(); + recButton.bindLight(layer, () -> transport.isArrangerRecordEnabled().get() ? RgbState.RED : RgbState.RED_LO); + recButton.bindPressed(layer, this::handleRec); + + final LaunchButton overdubButton = buttons.get(3); + transport.isArrangerOverdubEnabled().markInterested(); + overdubButton.bindLight( + layer, () -> transport.isArrangerOverdubEnabled().get() ? RgbState.ORANGE : RgbState.ORANGE_LO); + overdubButton.bindPressed(layer, this::overdubArranger); + + final LaunchButton overdubLauncherButton = buttons.get(4); + transport.isClipLauncherOverdubEnabled().markInterested(); + overdubLauncherButton.bindLight( + layer, () -> transport.isClipLauncherOverdubEnabled().get() ? RgbState.RED : RgbState.RED_LO); + overdubLauncherButton.bindPressed(layer, this::overdubLauncher); + + for (int i = 5; i < 7; i++) { + final LaunchButton button = buttons.get(i); + button.bindLight(layer, () -> RgbState.OFF); + button.bindPressed(layer, () -> {}); + } + + final LaunchButton sceneButton = buttons.get(7); + sceneButton.bindLight(layer, () -> RgbState.BLUE); + sceneButton.bindIsPressed(layer, this::handleSceneLaunch); + } + + private void overdubLauncher() { + if (shiftState.get()) { + displayControl.show2LineTemporary("Button", "Launcher Overdub"); + } else { + transport.isClipLauncherOverdubEnabled().toggle(); + } + } + + private void handleSceneLaunch(final boolean pressed) { + if (shiftState.get()) { + displayControl.show2LineTemporary("Button", "Scene %s".formatted(focusScene.name().get())); + } else { + if (pressed) { + focusScene.launch(); + } else { + focusScene.launchRelease(); + } + } + } + + private void handlePlay() { + if (shiftState.get()) { + displayControl.show2LineTemporary("Button", "Play"); + } else { + transport.play(); + } + } + + private void handleRec() { + if (shiftState.get()) { + displayControl.show2LineTemporary("Button", "Rec"); + } else { + transport.record(); + } + } + + private void handleStop() { + if (shiftState.get()) { + displayControl.show2LineTemporary("Button", "Stop"); + } else { + transport.stop(); + } + } + + private void overdubArranger() { + if (shiftState.get()) { + displayControl.show2LineTemporary("Button", "Arranger Overdub"); + } else { + transport.isArrangerOverdubEnabled().toggle(); + } + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/XlDawControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/XlDawControlLayer.java new file mode 100644 index 00000000..2fe08b99 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/XlDawControlLayer.java @@ -0,0 +1,102 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.CcConstValues; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.SegmentDisplayBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchButton; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.values.LayoutType; + +@Component(tag = "XLModel") +public class XlDawControlLayer extends AbstractDawControlLayer { + + private final Layer specLauncherLayer; + + public XlDawControlLayer(final Layers layers, final ControllerHost host, final LaunchControlXlHwElements hwElements, + final LaunchViewControl viewControl, final DisplayControl displayControl, + final TransportHandler transportHandler) { + super(layers, hwElements, viewControl, displayControl, transportHandler, host); + this.specLauncherLayer = new Layer(layers, "SPEC_LAUNCHER"); + deviceRemotes.bind(this, hwElements, displayControl); + + bindNavigation(hwElements); + transportHandler.bindTrackNavigation(this); + transportHandler.bindControl(this, hwElements, 2); + transportHandler.bindArrangerLayoutControl(this, hwElements, 2); + transportHandler.bindLauncherLayoutControl(specLauncherLayer, hwElements, 2); + transportHandler.getPanelLayout().addValueObserver(this::handlePanelLayoutUpdate); + transportHandler.setTrackNavigation(this::navigateTracks); + + selectTrackBinding = + new SegmentDisplayBinding("Select Track", cursorTrack.name(), displayControl.getTemporaryDisplay()); + this.addBinding(selectTrackBinding); + } + + + protected void handlePanelLayoutUpdate(final LayoutType newValue) { + if (isActive()) { + specLauncherLayer.setIsActive(newValue == LayoutType.LAUNCHER); + } + } + + private void bindNavigation(final LaunchControlXlHwElements hwElements) { + final LaunchButton pageUpButton = hwElements.getButtons(CcConstValues.PAGE_UP); + final LaunchButton pageDownButton = hwElements.getButtons(CcConstValues.PAGE_DOWN); + pageUpButton.bindLight(this, this::canPageNavigateBackward); + pageDownButton.bindLight(this, this::canPageNavigateForward); + pageUpButton.bindRepeatHold(this, this::navigateBackward); + pageDownButton.bindRepeatHold(this, this::navigateForward); + } + + private RgbState canPageNavigateBackward() { + if (shiftState.get()) { + return cursorDevice.hasPrevious().get() ? RgbState.DIM_WHITE : RgbState.OFF; + } + return deviceRemotes.canGoBack() ? RgbState.DIM_WHITE : RgbState.OFF; + } + + private RgbState canPageNavigateForward() { + if (shiftState.get()) { + return cursorDevice.hasNext().get() ? RgbState.DIM_WHITE : RgbState.OFF; + } + return deviceRemotes.canGoForward() ? RgbState.DIM_WHITE : RgbState.OFF; + } + + private void navigateBackward() { + if (shiftState.get()) { + cursorDevice.selectPrevious(); + } else { + deviceRemotes.selectPreviousPage(); + } + displayControl.cancelTemporary(); + } + + private void navigateForward() { + if (shiftState.get()) { + cursorDevice.selectNext(); + } else { + deviceRemotes.selectNextPage(); + } + displayControl.cancelTemporary(); + } + + @Override + protected void onActivate() { + super.onActivate(); + specLauncherLayer.setIsActive(transportHandler.getPanelLayout().get() == LayoutType.LAUNCHER); + deviceRemotes.setActive(true); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + specLauncherLayer.setIsActive(false); + deviceRemotes.setActive(false); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/XlMixerLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/XlMixerLayer.java new file mode 100644 index 00000000..ce9bf014 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchcontrolxlmk3/layer/XlMixerLayer.java @@ -0,0 +1,224 @@ +package com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.layer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Send; +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; +import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.CcConstValues; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlMidiProcessor; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlHwElements; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchViewControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.AbsoluteEncoderBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.DisplayId; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.LightSendValueBindings; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.LightValueBindings; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.ParameterDisplayBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.RelativeEncoderBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.SegmentDisplayBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.bindings.SliderBinding; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchAbsoluteEncoder; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchButton; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.control.LaunchRelativeEncoder; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.DisplayControl; +import com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.display.GradientColor; +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.di.Inject; + +@Component(tag = "XLModel") +public class XlMixerLayer extends AbstractMixerLayer { + + @Inject + private XlDawControlLayer dawLayer; + + + private Row1ButtonMode row1Mode = Row1ButtonMode.SOLO; + private Row2ButtonMode row2Mode = Row2ButtonMode.SELECT; + + private enum Row1ButtonMode { + SOLO, + ARM + } + + private enum Row2ButtonMode { + SELECT, + MUTE + } + + public XlMixerLayer(final Layers layers, final LaunchControlXlHwElements hwElements, + final LaunchViewControl viewControl, final DisplayControl displayControl, + final LaunchControlMidiProcessor midiProcessor, final ControllerHost host, + final TransportHandler transportHandler, final ButtonLayers buttonLayers) { + super(layers, midiProcessor, host, viewControl, hwElements, displayControl, transportHandler, buttonLayers); + + final TrackBank trackBank = viewControl.getTrackBank(); + for (int i = 0; i < 8; i++) { + bindTrack(hwElements, trackBank, i); + } + + bindNavigation(hwElements); + transportHandler.bindTransport(this); + final LaunchButton soloArmButton = hwElements.getButtons(CcConstValues.SOLO_ARM_MODE); + final LaunchButton muteSelectButton = hwElements.getButtons(CcConstValues.MUTE_SELECT_MODE); + + final LaunchButton specModeButton = hwElements.getButtons(CcConstValues.DAW_SPEC); + specModeButton.bindLight(this, () -> RgbState.BLUE); + //specModeButton.bindPressed(this, () -> LaunchControlXlMk3Extension.println(" > PRESS SPEC >")); + + soloArmButton.bindLight(this, () -> row1Mode == Row1ButtonMode.ARM ? RgbState.RED : RgbState.YELLOW); + muteSelectButton.bindLight(this, () -> row2Mode == Row2ButtonMode.SELECT ? RgbState.WHITE : RgbState.ORANGE); + soloArmButton.bindPressed(this, this::toggleSoloArmMode); + muteSelectButton.bindPressed(this, this::toggleSelectMuteMode); + } + + @Activate + public void init() { + this.setIsActive(true); + applyMode(); + } + + private void bindNavigation(final LaunchControlXlHwElements hwElements) { + final LaunchButton pageUpButton = hwElements.getButtons(CcConstValues.PAGE_UP); + final LaunchButton pageDownButton = hwElements.getButtons(CcConstValues.PAGE_DOWN); + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + transportHandler.bindTrackNavigation(mixerLayer); + + mixerLayer.addBinding( + new SegmentDisplayBinding("Select Track", cursorTrack.name(), displayControl.getFixedDisplay())); + + final SendBank refBank = viewControl.getRefSendBank(); + final Send send1 = refBank.getItemAt(0); + final Send send2 = refBank.getItemAt(1); + send1.name().markInterested(); + send2.name().markInterested(); + refBank.canScrollBackwards().markInterested(); + refBank.canScrollForwards().markInterested(); + + pageUpButton.bindLight(mixerLayer, () -> refBank.canScrollBackwards().get() ? RgbState.WHITE : RgbState.OFF); + pageDownButton.bindLight(mixerLayer, () -> refBank.canScrollForwards().get() ? RgbState.WHITE : RgbState.OFF); + pageUpButton.bindRepeatHold(mixerLayer, () -> viewControl.navigateSends(-1)); + pageDownButton.bindRepeatHold(mixerLayer, () -> viewControl.navigateSends(1)); + refBank.scrollPosition().addValueObserver(pos -> displayControl.show2LineTemporary( + "Sends", + "%s - %s".formatted(send1.name().get(), send2.name().get()))); + } + + + private void bindTrack(final LaunchControlXlHwElements hwElements, final TrackBank trackBank, final int index) { + final Track track = trackBank.getItemAt(index); + final Send send1 = track.sendBank().getItemAt(0); + final Send send2 = track.sendBank().getItemAt(1); + track.color().addValueObserver((r, g, b) -> changeTrackColor(index, ColorLookup.toColor(r, g, b))); + track.addIsSelectedInMixerObserver(select -> { + if (select) { + this.selectedTrackIndex.set(index); + } + }); + track.arm().markInterested(); + track.exists().markInterested(); + track.mute().markInterested(); + track.solo().markInterested(); + + final LaunchAbsoluteEncoder row1Encoder = hwElements.getAbsoluteEncoder(0, index); + final LaunchAbsoluteEncoder row2Encoder = hwElements.getAbsoluteEncoder(1, index); + final LaunchRelativeEncoder row3Encoder = hwElements.getRelativeEncoder(2, index); + + final ParameterDisplayBinding send1DisplayBinding = + new ParameterDisplayBinding(new DisplayId(row1Encoder.getTargetId(), displayControl), track.name(), send1); + mixerLayer.addBinding(send1DisplayBinding); + mixerLayer.addBinding(new AbsoluteEncoderBinding(send1, row1Encoder)); + mixerLayer.addBinding(new LightSendValueBindings(send1, row1Encoder.getLight())); + + final ParameterDisplayBinding send2DisplayBinding = + new ParameterDisplayBinding(new DisplayId(row2Encoder.getTargetId(), displayControl), track.name(), send2); + mixerLayer.addBinding(send2DisplayBinding); + mixerLayer.addBinding(new LightSendValueBindings(send2, row2Encoder.getLight())); + mixerLayer.addBinding(new AbsoluteEncoderBinding(send2, hwElements.getAbsoluteEncoder(1, index))); + + // fixedPanLabel + final ParameterDisplayBinding panDisplayBinding = + new ParameterDisplayBinding( + new DisplayId(row3Encoder.getTargetId(), displayControl), track.name(), track.pan()); + mixerLayer.addBinding(panDisplayBinding); + mixerLayer.addBinding(new LightValueBindings(track.pan(), row3Encoder.getLight(), GradientColor.PAN)); + mixerLayer.addBinding(new RelativeEncoderBinding(track.pan(), row3Encoder)); + + // fixedVolumeLabel + final ParameterDisplayBinding volumeDisplayBinding = + new ParameterDisplayBinding( + new DisplayId(row2Encoder.getTargetId(), displayControl), track.name(), track.volume()); + mixerLayer.addBinding(volumeDisplayBinding); + this.addBinding(new SliderBinding(row2Encoder.getId(), track.volume(), hwElements.getSlider(index))); + + final LaunchButton row2Button = hwElements.getRowButtons(1, index); + final LaunchButton row1Button = hwElements.getRowButtons(0, index); + row1Button.bindLight(buttonLayers.getArmLayer(), () -> armColor(track)); + row1Button.bindIsPressed(buttonLayers.getArmLayer(), pressed -> toggleArm(pressed, track)); + row1Button.bindLight(buttonLayers.getSoloLayer(), () -> soloColor(track)); + row1Button.bindIsPressed(buttonLayers.getSoloLayer(), pressed -> toggleSolo(pressed, track)); + + row2Button.bindLight(buttonLayers.getSelectLayer(), () -> selectColor(track, index)); + row2Button.bindPressed(buttonLayers.getSelectLayer(), () -> selectTrack(track)); + row2Button.bindLight(buttonLayers.getMuteLayer(), () -> muteColor(track)); + row2Button.bindPressed(buttonLayers.getMuteLayer(), () -> track.mute().toggle()); + } + + private void toggleSoloArmMode() { + if (this.row1Mode == Row1ButtonMode.ARM) { + this.row1Mode = Row1ButtonMode.SOLO; + displayControl.show2LineTemporary("Arm/Solo", "Solo"); + } else { + this.row1Mode = Row1ButtonMode.ARM; + displayControl.show2LineTemporary("Arm/Solo", "Arm"); + } + applyArmSoloMode(); + } + + private void toggleSelectMuteMode() { + if (this.row2Mode == Row2ButtonMode.SELECT) { + this.row2Mode = Row2ButtonMode.MUTE; + displayControl.show2LineTemporary("Mute/Select", "Mute"); + } else { + this.row2Mode = Row2ButtonMode.SELECT; + displayControl.show2LineTemporary("Mute/Select", "Select"); + } + applySelectMuteMode(); + } + + protected void applyMode() { + if (mode == BaseMode.MIXER) { + midiProcessor.setToRelative(0, false); + midiProcessor.setToRelative(1, false); + midiProcessor.setToRelative(2, true); + } else { + midiProcessor.setToRelative(0, true); + midiProcessor.setToRelative(1, true); + midiProcessor.setToRelative(2, true); + } + this.mixerLayer.setIsActive(mode == BaseMode.MIXER); + this.dawLayer.setIsActive(mode == BaseMode.DAW); + + + applySelectMuteMode(); + applyArmSoloMode(); + + this.buttonLayers.getSelectLayer().setIsActive(true); + + } + + private void applyArmSoloMode() { + this.buttonLayers.getSoloLayer().setIsActive(row1Mode == Row1ButtonMode.SOLO); + this.buttonLayers.getArmLayer().setIsActive(row1Mode == Row1ButtonMode.ARM); + } + + private void applySelectMuteMode() { + this.buttonLayers.getSelectLayer().setIsActive(row2Mode == Row2ButtonMode.SELECT); + this.buttonLayers.getMuteLayer().setIsActive(row2Mode == Row2ButtonMode.MUTE); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/SessionLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/SessionLayer.java index b13ccfc1..3cd9b71a 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/SessionLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/SessionLayer.java @@ -15,6 +15,7 @@ import com.bitwig.extension.controller.api.Transport; import com.bitwig.extensions.controllers.novation.commonsmk3.AbstractLpSessionLayer; import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; +import com.bitwig.extensions.controllers.novation.commonsmk3.FocusSlot; import com.bitwig.extensions.controllers.novation.commonsmk3.GridButton; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.LaunchPadButton; @@ -23,11 +24,10 @@ import com.bitwig.extensions.controllers.novation.commonsmk3.PanelLayout; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; -import com.bitwig.extensions.controllers.novation.launchpadmini3.LpMiniHwElements; import com.bitwig.extensions.controllers.novation.launchpadmini3.LabelCcAssignmentsMini; +import com.bitwig.extensions.controllers.novation.launchpadmini3.LpMiniHwElements; import com.bitwig.extensions.controllers.novation.launchpadmini3.LpMode; import com.bitwig.extensions.controllers.novation.launchpadmini3.TrackMode; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.FocusSlot; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Activate; @@ -372,69 +372,84 @@ private void initTrackControlSceneButtons(final LpMiniHwElements hwElements, fin private void initVolumeControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton volumeButton = hwElements.getSceneLaunchButtons().get(index); - volumeButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.VOLUME), this::returnToPreviousMode, + volumeButton.bindPressReleaseAfter( + this, () -> intoControlMode(ControlMode.VOLUME), this::returnToPreviousMode, MOMENTARY_TIME); - volumeButton.bindLight(layer, + volumeButton.bindLight( + layer, () -> controlMode == ControlMode.VOLUME ? RgbState.of(9) : RgbState.of(MODE_INACTIVE_COLOR)); } private void initPanControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton panButton = hwElements.getSceneLaunchButtons().get(index); - panButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.PAN), this::returnToPreviousMode, + panButton.bindPressReleaseAfter( + this, () -> intoControlMode(ControlMode.PAN), this::returnToPreviousMode, MOMENTARY_TIME); - panButton.bindLight(layer, + panButton.bindLight( + layer, () -> controlMode == ControlMode.PAN ? RgbState.of(9) : RgbState.of(MODE_INACTIVE_COLOR)); } private void initSendsAControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton sendsAButton = hwElements.getSceneLaunchButtons().get(index); - sendsAButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.SENDS_A), this::returnToPreviousMode, + sendsAButton.bindPressReleaseAfter( + this, () -> intoControlMode(ControlMode.SENDS_A), this::returnToPreviousMode, MOMENTARY_TIME); sendsAButton.bindLight(layer, () -> getSendsState(ControlMode.SENDS_A)); } private void initSendsBControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton sendsBButton = hwElements.getSceneLaunchButtons().get(index); - sendsBButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.SENDS_B), this::returnToPreviousMode, + sendsBButton.bindPressReleaseAfter( + this, () -> intoControlMode(ControlMode.SENDS_B), this::returnToPreviousMode, MOMENTARY_TIME); sendsBButton.bindLight(layer, () -> getSendsState(ControlMode.SENDS_B)); } private void initDeviceControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton deviceButton = hwElements.getSceneLaunchButtons().get(index); - deviceButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.DEVICE), this::returnToPreviousMode, + deviceButton.bindPressReleaseAfter( + this, () -> intoControlMode(ControlMode.DEVICE), this::returnToPreviousMode, MOMENTARY_TIME); - deviceButton.bindLight(layer, + deviceButton.bindLight( + layer, () -> controlMode == ControlMode.DEVICE ? RgbState.of(33) : RgbState.of(MODE_INACTIVE_COLOR)); } private void initStopControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton stopButton = hwElements.getSceneLaunchButtons().get(index); - stopButton.bindPressReleaseAfter(this, () -> intoTrackMode(TrackMode.STOP), this::returnToPreviousMode, + stopButton.bindPressReleaseAfter( + this, () -> intoTrackMode(TrackMode.STOP), this::returnToPreviousMode, MOMENTARY_TIME); - stopButton.bindLight(layer, + stopButton.bindLight( + layer, () -> trackMode == TrackMode.STOP ? RgbState.of(5) : RgbState.of(MODE_INACTIVE_COLOR)); } private void initMuteControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton muteButton = hwElements.getSceneLaunchButtons().get(index); - muteButton.bindPressReleaseAfter(this, () -> intoTrackMode(TrackMode.MUTE), this::returnToPreviousMode, + muteButton.bindPressReleaseAfter( + this, () -> intoTrackMode(TrackMode.MUTE), this::returnToPreviousMode, MOMENTARY_TIME); - muteButton.bindLight(layer, + muteButton.bindLight( + layer, () -> trackMode == TrackMode.MUTE ? RgbState.of(9) : RgbState.of(MODE_INACTIVE_COLOR)); } private void initSoloControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton soloButton = hwElements.getSceneLaunchButtons().get(index); - soloButton.bindPressReleaseAfter(this, () -> intoTrackMode(TrackMode.SOLO), this::returnToPreviousMode, + soloButton.bindPressReleaseAfter( + this, () -> intoTrackMode(TrackMode.SOLO), this::returnToPreviousMode, MOMENTARY_TIME); - soloButton.bindLight(layer, + soloButton.bindLight( + layer, () -> trackMode == TrackMode.SOLO ? RgbState.of(13) : RgbState.of(MODE_INACTIVE_COLOR)); } private void initArmControl(final LpMiniHwElements hwElements, final Layer layer, final int index) { final LabeledButton soloButton = hwElements.getSceneLaunchButtons().get(index); - soloButton.bindPressReleaseAfter(this, () -> intoTrackMode(TrackMode.ARM), this::returnToPreviousMode, + soloButton.bindPressReleaseAfter( + this, () -> intoTrackMode(TrackMode.ARM), this::returnToPreviousMode, MOMENTARY_TIME); soloButton.bindLight( layer, () -> trackMode == TrackMode.ARM ? RgbState.of(5) : RgbState.of(MODE_INACTIVE_COLOR)); @@ -486,7 +501,7 @@ public void intoControlMode(final ControlMode mode) { controlMode = mode; if (modeLayer != null) { modeLayer.setIsActive(true); - if (modeLayer instanceof SendsSliderLayer sendsSliderLayer) { + if (modeLayer instanceof final SendsSliderLayer sendsSliderLayer) { sendsSliderLayer.setControl(mode); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LaunchpadProMk3ControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LaunchpadProMk3ControllerExtension.java index 20bd23dc..86fd59ea 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LaunchpadProMk3ControllerExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LaunchpadProMk3ControllerExtension.java @@ -19,6 +19,7 @@ import com.bitwig.extension.controller.api.PinnableCursorDevice; import com.bitwig.extension.controller.api.SettableEnumValue; import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.novation.commonsmk3.FocusSlot; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.LaunchpadDeviceConfig; import com.bitwig.extensions.controllers.novation.commonsmk3.LpHwElements; @@ -93,7 +94,8 @@ public void init() { midiIn2.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi1); final MidiOut midiOut = host.getMidiOutPort(0); - final MidiProcessor midiProcessor = new MidiProcessor(host, midiIn, midiOut, + final MidiProcessor midiProcessor = new MidiProcessor( + host, midiIn, midiOut, new LaunchpadDeviceConfig("LaunchPadProMk3", 0x0E, 0xB4, 0xB5, false)); diContext.registerService(MidiProcessor.class, midiProcessor); @@ -170,10 +172,11 @@ private void changeDrumMode(final boolean drumModeActive) { private void assignModifiers(final Application application) { final LabeledButton shiftButton = hwElements.getLabeledButton(LabelCcAssignments.SHIFT); - shiftButton.bindPressed(mainLayer, pressed -> { - shiftLayer.setIsActive(pressed); - modifierStates.setShift(pressed); - }, RgbState.BLUE, RgbState.WHITE); + shiftButton.bindPressed( + mainLayer, pressed -> { + shiftLayer.setIsActive(pressed); + modifierStates.setShift(pressed); + }, RgbState.BLUE, RgbState.WHITE); final LabeledButton clearButton = hwElements.getLabeledButton(LabelCcAssignments.CLEAR); clearButton.bindPressed(mainLayer, this::handleClear); @@ -191,7 +194,8 @@ private void assignModifiers(final Application application) { final SettableEnumValue recordQuantizeValue = application.recordQuantizationGrid(); recordQuantizeValue.markInterested(); quantizeButton.bindPressed(shiftLayer, pressed -> toggleRecordQuantize(recordQuantizeValue, pressed)); - quantizeButton.bindLight(shiftLayer, + quantizeButton.bindLight( + shiftLayer, () -> recordQuantizeValue.get().equals("OFF") ? RgbState.RED_LO : RgbState.TURQUOISE); } @@ -230,29 +234,34 @@ private void toggleRecordQuantize(final SettableEnumValue recordQuant, final Boo private void assignModeButtons() { final LabeledButton sessionButton = hwElements.getLabeledButton(LabelCcAssignments.SESSION); - sessionButton.bindRelease(mainLayer, () -> { - if (overviewLayer.isActive()) { - overviewLayer.setIsActive(false); - } - }); - sessionButton.bindPressed(mainLayer, () -> { - if (mainMode == LpBaseMode.SESSION) { - overviewLayer.setIsActive(true); - } else { - sysExHandler.changeMode(LpBaseMode.SESSION, 0); - } - }); - sessionButton.bindLight(mainLayer, + sessionButton.bindRelease( + mainLayer, () -> { + if (overviewLayer.isActive()) { + overviewLayer.setIsActive(false); + } + }); + sessionButton.bindPressed( + mainLayer, () -> { + if (mainMode == LpBaseMode.SESSION) { + overviewLayer.setIsActive(true); + } else { + sysExHandler.changeMode(LpBaseMode.SESSION, 0); + } + }); + sessionButton.bindLight( + mainLayer, () -> sysExHandler.getMode() == LpBaseMode.SESSION ? RgbState.BLUE : RgbState.DIM_WHITE); final LabeledButton noteButton = hwElements.getLabeledButton(LabelCcAssignments.NOTE); - noteButton.bind(mainLayer, () -> { - sysExHandler.changeMode(LpBaseMode.NOTE, 0); - }, () -> sysExHandler.getMode() == LpBaseMode.NOTE ? RgbState.ORANGE : RgbState.DIM_WHITE); + noteButton.bind( + mainLayer, () -> { + sysExHandler.changeMode(LpBaseMode.NOTE, 0); + }, () -> sysExHandler.getMode() == LpBaseMode.NOTE ? RgbState.ORANGE : RgbState.DIM_WHITE); final LabeledButton chordButton = hwElements.getLabeledButton(LabelCcAssignments.CHORD); - chordButton.bind(mainLayer, () -> { - sysExHandler.changeMode(LpBaseMode.CHORD, 0); - }, () -> sysExHandler.getMode() == LpBaseMode.CHORD ? RgbState.ORANGE : RgbState.DIM_WHITE); + chordButton.bind( + mainLayer, () -> { + sysExHandler.changeMode(LpBaseMode.CHORD, 0); + }, () -> sysExHandler.getMode() == LpBaseMode.CHORD ? RgbState.ORANGE : RgbState.DIM_WHITE); } private void initTransportSection() { @@ -263,14 +272,16 @@ private void initTransportSection() { final LabeledButton playButton = hwElements.getLabeledButton(LabelCcAssignments.PLAY); playButton.bind(mainLayer, this::togglePlay, this::getPlayColor); - playButton.bind(shiftLayer, () -> transport.continuePlayback(), + playButton.bind( + shiftLayer, () -> transport.continuePlayback(), () -> transport.isPlaying().get() ? RgbState.TURQUOISE : RgbState.SHIFT_INACTIVE); final LabeledButton recButton = hwElements.getLabeledButton(LabelCcAssignments.REC); recButton.bind(mainLayer, this::toggleRecord, this::getRecordButtonColorRegular); recButton.bindPressed(shiftLayer, () -> transport.isClipLauncherOverdubEnabled().toggle()); - recButton.bindLight(shiftLayer, pressed -> transport.isClipLauncherOverdubEnabled().get() ? // - (pressed ? RgbState.pulse(60) : RgbState.of(60)) : RgbState.DIM_WHITE); + recButton.bindLight( + shiftLayer, pressed -> transport.isClipLauncherOverdubEnabled().get() ? // + (pressed ? RgbState.pulse(60) : RgbState.of(60)) : RgbState.DIM_WHITE); } private RgbState getPlayColor() { diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/SessionLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/SessionLayer.java index 1f9e84d1..8193e351 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/SessionLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/SessionLayer.java @@ -10,12 +10,12 @@ import com.bitwig.extension.controller.api.Transport; import com.bitwig.extensions.controllers.novation.commonsmk3.AbstractLpSessionLayer; import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; +import com.bitwig.extensions.controllers.novation.commonsmk3.FocusSlot; import com.bitwig.extensions.controllers.novation.commonsmk3.GridButton; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.PanelLayout; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.FocusSlot; import com.bitwig.extensions.controllers.novation.launchpadpromk3.LabelCcAssignments; import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpProHwElements; import com.bitwig.extensions.controllers.novation.launchpadpromk3.LppPreferences; diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/slmk3/layer/EncoderSelectionLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/slmk3/layer/EncoderSelectionLayer.java index d979b06b..4450196c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/slmk3/layer/EncoderSelectionLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/slmk3/layer/EncoderSelectionLayer.java @@ -54,14 +54,8 @@ @Component public class EncoderSelectionLayer { private final SlRgbState[] DEVICE_COLORS = { - SlRgbState.RED, - SlRgbState.ORANGE, - SlRgbState.YELLOW, - SlRgbState.GREEN, - SlRgbState.DARK_GREEN, - SlRgbState.BLUE, - SlRgbState.PURPLE, - SlRgbState.PINK + SlRgbState.RED, SlRgbState.ORANGE, SlRgbState.YELLOW, SlRgbState.GREEN, SlRgbState.DARK_GREEN, SlRgbState.BLUE, + SlRgbState.PURPLE, SlRgbState.PINK }; private final SlRgbState[] trackColors = new SlRgbState[8]; private final ViewControl viewControl; @@ -118,11 +112,14 @@ public EncoderSelectionLayer(final ViewControl viewControl, final ScreenHandler selectButton.bindPressed(trackButtonSelectionLayer, () -> handleTrackPressed(track)); } - bindRemotes(layerRepo.getKnobLayer(KnobMode.DEVICE), screenHandler.getScreen(KnobMode.DEVICE), + bindRemotes( + layerRepo.getKnobLayer(KnobMode.DEVICE), screenHandler.getScreen(KnobMode.DEVICE), viewControl.getPrimaryRemotes(), i -> DEVICE_COLORS[i]); - bindRemotes(layerRepo.getKnobLayer(KnobMode.TRACK), screenHandler.getScreen(KnobMode.TRACK), + bindRemotes( + layerRepo.getKnobLayer(KnobMode.TRACK), screenHandler.getScreen(KnobMode.TRACK), viewControl.getTrackRemotes(), i -> DEVICE_COLORS[i]); - bindRemotes(layerRepo.getKnobLayer(KnobMode.PROJECT), screenHandler.getScreen(KnobMode.PROJECT), + bindRemotes( + layerRepo.getKnobLayer(KnobMode.PROJECT), screenHandler.getScreen(KnobMode.PROJECT), viewControl.getProjectRemotes(), i -> DEVICE_COLORS[i]); bindPanControl(trackBank, encoders); bindSendControl(trackBank, encoders); @@ -165,27 +162,33 @@ private void bindShiftOption() { transport.clipLauncherPostRecordingAction().addValueObserver(v -> postRecordingValues.setToValue(v)); final SlRgbState frameColor = SlRgbState.ORANGE; final SettableRangedValue metroVolume = transport.metronomeVolume(); - encoderLayer.addBinding(new BoxPanelBinding(transport.tempo().displayedValue(), screen.getPanel(index), + encoderLayer.addBinding(new BoxPanelBinding( + transport.tempo().displayedValue(), screen.getPanel(index), new BasicStringValue("Tempo"), frameColor)); - encoders.get(index++).bindIncrementAction(encoderLayer, + encoders.get(index++).bindIncrementAction( + encoderLayer, inc -> incrementBy(transport.tempo(), inc, globalStates.getShiftState().get())); - encoderLayer.addBinding(new BoxPanelBinding(quantizeValues.getDisplayValue(), screen.getPanel(index), + encoderLayer.addBinding(new BoxPanelBinding( + quantizeValues.getDisplayValue(), screen.getPanel(index), new BasicStringValue("Rec.Qu"), frameColor)); encoders.get(index++).bindIncrementAction(encoderLayer, new IncrementHandler(quantizeValues::incrementBy, 10)); - encoderLayer.addBinding(new BoxPanelBinding(preRollValues.getDisplayValue(), screen.getPanel(index), + encoderLayer.addBinding(new BoxPanelBinding( + preRollValues.getDisplayValue(), screen.getPanel(index), new BasicStringValue("PreRoll"), frameColor)); encoders.get(index++).bindIncrementAction(encoderLayer, new IncrementHandler(preRollValues::incrementBy, 10)); - encoderLayer.addBinding(new BoxPanelBinding(postRecordingValues.getDisplayValue(), screen.getPanel(index), - new BasicStringValue("PstRecAc"), frameColor)); + encoderLayer.addBinding( + new BoxPanelBinding( + postRecordingValues.getDisplayValue(), screen.getPanel(index), new BasicStringValue("PstRecAc"), + frameColor)); encoders.get(index++) .bindIncrementAction(encoderLayer, new IncrementHandler(postRecordingValues::incrementBy, 10)); encoderLayer.addBinding( - new BoxPanelBinding(metroVolume.displayedValue(), screen.getPanel(index), new BasicStringValue("Metr.Vol"), - frameColor)); + new BoxPanelBinding( + metroVolume.displayedValue(), screen.getPanel(index), new BasicStringValue("Metr.Vol"), frameColor)); encoders.get(index++).bindParameter(encoderLayer, metroVolume); } @@ -387,13 +390,13 @@ private void bindDrumsMix(final LayerRepo layerRepo, final ScreenHandler screenH encoder.bind(drumSendsLayer, sendItem); drumVolumeLayer.addBinding( - new ParameterPanelBinding(pad.volume(), volumeScreen.getPanel(i), pad.name(), SlRgbState.WHITE, - trackColor)); + new ParameterPanelBinding( + pad.volume(), volumeScreen.getPanel(i), pad.name(), SlRgbState.WHITE, trackColor)); drumPanLayer.addBinding( new ParameterPanelBinding(pad.pan(), panScreen.getPanel(i), pad.name(), SlRgbState.ORANGE, trackColor)); drumSendsLayer.addBinding( - new ParameterPanelBinding(sendItem, sendsScreen.getPanel(i), pad.name(), SlRgbState.YELLOW, - trackColor)); + new ParameterPanelBinding( + sendItem, sendsScreen.getPanel(i), pad.name(), SlRgbState.YELLOW, trackColor)); } } @@ -415,8 +418,8 @@ private void bindRemotes(final Layer layer, final ScreenSetup knobScr encoder.bind(layer, parameter); encoder.bindEmpty(layer); layer.addBinding( - new ParameterPanelBinding(parameter, knobScreen.getPanel(i), parameter.name(), colorProvider.apply(i), - trackColor)); + new ParameterPanelBinding( + parameter, knobScreen.getPanel(i), parameter.name(), colorProvider.apply(i), trackColor)); } } @@ -508,24 +511,33 @@ private void setupShiftLayer(final SlMk3HardwareElements hwElements) { final SelectionSubPanel panel = shiftPanels.get(i); if (action != null) { switch (action) { - case UNDO -> assignAction(i, action, selectButton, application.canUndo(), application::undo, + case UNDO -> assignAction( + i, action, selectButton, application.canUndo(), application::undo, SlRgbState.BITWIG_ORANGE); - case REDO -> assignAction(i, action, selectButton, application.canRedo(), application::redo, + case REDO -> assignAction( + i, action, selectButton, application.canRedo(), application::redo, SlRgbState.BITWIG_ORANGE); - case CLICK -> assignToggleValue(i, action, selectButton, transport.isMetronomeEnabled(), + case CLICK -> assignToggleValue( + i, action, selectButton, transport.isMetronomeEnabled(), SlRgbState.BITWIG_ORANGE); case CL_OVERDUB -> - assignToggleValue(i, action, selectButton, transport.isClipLauncherOverdubEnabled(), + assignToggleValue( + i, action, selectButton, transport.isClipLauncherOverdubEnabled(), SlRgbState.BITWIG_ORANGE); case CL_AUTO -> - assignToggleValue(i, action, selectButton, transport.isClipLauncherAutomationWriteEnabled(), + assignToggleValue( + i, action, selectButton, transport.isClipLauncherAutomationWriteEnabled(), SlRgbState.BITWIG_ORANGE); case AR_AUTO -> - assignToggleValue(i, action, selectButton, transport.isArrangerAutomationWriteEnabled(), + assignToggleValue( + i, action, selectButton, transport.isArrangerAutomationWriteEnabled(), SlRgbState.BITWIG_ORANGE); - case AUTO_OVERRIDE -> assignAction(i, action, selectButton, transport.isAutomationOverrideActive(), - transport::resetAutomationOverrides, SlRgbState.BITWIG_ORANGE); - case FILL -> assignToggleValue(i, action, selectButton, transport.isFillModeActive(), + case AUTO_OVERRIDE -> + assignAction( + i, action, selectButton, transport.isAutomationOverrideActive(), + transport::resetAutomationOverrides, SlRgbState.BITWIG_ORANGE); + case FILL -> assignToggleValue( + i, action, selectButton, transport.isFillModeActive(), SlRgbState.BITWIG_ORANGE); } } else { @@ -559,7 +571,7 @@ private void assignToggleValue(final int index, final ShiftAction action, final selectButton.bindLightOnPressed(layer, color, value); } - private void handleTrackSelectionChanged(final int old, final int value) { + private void handleTrackSelectionChanged(final int value) { final ButtonSubPanel trackPanel = screenHandler.getSubPanel(ButtonMode.TRACK); for (int i = 0; i < 8; i++) { trackPanel.get(i).setSelected(value == i); diff --git a/src/main/java/com/bitwig/extensions/framework/di/Component.java b/src/main/java/com/bitwig/extensions/framework/di/Component.java index 31683a7f..18335ad9 100644 --- a/src/main/java/com/bitwig/extensions/framework/di/Component.java +++ b/src/main/java/com/bitwig/extensions/framework/di/Component.java @@ -11,7 +11,9 @@ @Retention(RUNTIME) @Target(TYPE) public @interface Component { - String name() default ""; - - int priority() default 1; + String name() default ""; + + int priority() default 1; + + String tag() default ""; } diff --git a/src/main/java/com/bitwig/extensions/framework/di/Context.java b/src/main/java/com/bitwig/extensions/framework/di/Context.java index b18bcbb0..9cffb3ea 100644 --- a/src/main/java/com/bitwig/extensions/framework/di/Context.java +++ b/src/main/java/com/bitwig/extensions/framework/di/Context.java @@ -1,510 +1,547 @@ package com.bitwig.extensions.framework.di; -import com.bitwig.extension.controller.ControllerExtension; -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.framework.Layer; -import com.bitwig.extensions.framework.Layers; - import java.io.IOException; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; -import java.lang.reflect.*; -import java.util.*; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import com.bitwig.extension.controller.ControllerExtension; +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.Project; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; /** * A Dependency Inject context to be used within a Bitwig Extension content. */ public class Context { - - private static ViewTracker viewTrackerGlobal = null; - private final Map, Class> serviceTypes = new HashMap<>(); - private final Map, Object> services = new HashMap<>(); - private final List> incompleteClosures = new ArrayList<>(); - private final List> incompleteSetterClosures = new ArrayList<>(); - private static int counter = 0; - - private static class ComponentClosure { - final Class clazz; - final Set> missingComponents = new HashSet<>(); - final Set> missingSetters = new HashSet<>(); - T instance; - - ComponentClosure(final Class clazz) { - this.clazz = clazz; - } - } - - /** - * Currently experimental. For linked controls. - * - * @param host currently for timely output - */ - public static void registerCounter(final ControllerHost host) { - host.println(" REGISTER " + counter++); - } - - /** - * Creates the dependency injection context from the controller extension. Creates and registers the - * following bitwig elements as services: - *
    - *
  • {@link Transport}
  • - *
  • {@link ControllerHost}
  • - *
  • {@link Application}
  • - *
  • {@link HardwareSurface}
  • - *
  • a layers object {@link Layers}
  • - *
- * The package of the extension and all sub-packages are scanned for other classes annotated with the - * {@link Component} annotation. - * Another way to create services is to create them via the {@link Context#create(Class)} method. - * This instantiates the component and registers it as Service in the context. The service is access by - * the class of the component. - * Further more objects can be registered as services via {@link Context#create(Class)} or - * {@link Context#registerService(Class, Object)} - * - * @param extension the controller extension. - */ - public Context(final ControllerExtension extension, final Package... packages) { - initializeExtension(extension); - scanPackage(extension.getClass(), packages); - } - - public void activate() { - invoke(Activate.class); - } - - public void deactivate() { - invoke(Deactivate.class); - } - - private void invoke(final Class annotation) { - final HashSet serviced = new HashSet<>(); - final Collection components = services.values(); - for (final Object component : components) { - if (!serviced.contains(component) && invokeActivation(component, annotation)) { - serviced.add(component); - } - } - } - - private boolean invokeActivation(final Object comp, final Class annotation) { - final Class clazz = comp.getClass(); - final Method[] methods = clazz.getMethods(); - for (final Method method : methods) { - final T activateAnnotation = method.getAnnotation(annotation); - if (activateAnnotation != null && method.getParameterCount() == 0) { - try { - method.invoke(comp); - return true; - } catch (final IllegalAccessException | InvocationTargetException exception) { - throw new DiException("failed to activate component " + clazz.getName(), exception); + + private static ViewTracker viewTrackerGlobal = null; + private final Map, Class> serviceTypes = new HashMap<>(); + private final Map, Object> services = new HashMap<>(); + private final List> incompleteClosures = new ArrayList<>(); + private final List> incompleteSetterClosures = new ArrayList<>(); + private final Set tags; + private static int counter = 0; + + private static class ComponentClosure { + final Class clazz; + final Set> missingComponents = new HashSet<>(); + final Set> missingSetters = new HashSet<>(); + T instance; + + ComponentClosure(final Class clazz) { + this.clazz = clazz; + } + } + + /** + * Currently experimental. For linked controls. + * + * @param host currently for timely output + */ + public static void registerCounter(final ControllerHost host) { + host.println(" REGISTER " + counter++); + } + + /** + * Creates the dependency injection context from the controller extension. Creates and registers the + * following bitwig elements as services: + *
    + *
  • {@link Transport}
  • + *
  • {@link ControllerHost}
  • + *
  • {@link Application}
  • + *
  • {@link HardwareSurface}
  • + *
  • a layers object {@link Layers}
  • + *
+ * The package of the extension and all sub-packages are scanned for other classes annotated with the + * {@link Component} annotation. + * Another way to create services is to create them via the {@link Context#create(Class)} method. + * This instantiates the component and registers it as Service in the context. The service is access by + * the class of the component. + * Further more objects can be registered as services via {@link Context#create(Class)} or + * {@link Context#registerService(Class, Object)} + * + * @param extension the controller extension. + */ + public Context(final ControllerExtension extension, final Package... packages) { + this(extension, Set.of(), packages); + } + + public Context(final ControllerExtension extension, final Set tags, final Package... packages) { + this.tags = tags; + initializeExtension(extension); + scanPackage(extension.getClass(), packages); + } + + + public void activate() { + invoke(Activate.class); + } + + public void deactivate() { + invoke(Deactivate.class); + } + + private void invoke(final Class annotation) { + final HashSet serviced = new HashSet<>(); + final Collection components = services.values(); + for (final Object component : components) { + if (!serviced.contains(component) && invokeActivation(component, annotation)) { + serviced.add(component); } - } - } - return false; - } - - private record ComponentClassBind(Class clazz, Component component) implements Comparable { - // - static Optional create(final Class clazz) { - final Component serviceAnnotation = clazz.getAnnotation(Component.class); - if (serviceAnnotation != null && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) { - return Optional.of(new ComponentClassBind(clazz, serviceAnnotation)); - } - return Optional.empty(); - } - - @Override - public int compareTo(final ComponentClassBind o) { - return o.component.priority() - component.priority(); - } - } - - private void scanPackage(final Class baseClass, final Package... packages) { - try { - final List> classes = PackageHelper.getClasses(baseClass, packages); - final List components = classes.stream()// - .map(ComponentClassBind::create) // - .flatMap(o -> o.stream()).sorted() // - .toList(); - for (final ComponentClassBind bind : components) { - createAnnotatedComponent(bind.clazz()); - registerInterfacesToService(bind.clazz()); - } - } catch (final IOException | ClassNotFoundException exception) { - throw new DiException("Failed to create annotated components", exception); - } - } - - private void registerInterfacesToService(final Class clazz) { - final List> classInterfaces = getInterfaces(clazz); - for (final Class interfaceClass : classInterfaces) { - serviceTypes.put(interfaceClass, clazz); - } - } - - private void initializeExtension(final ControllerExtension extension) { - final ControllerHost host = extension.getHost(); - registerService(ControllerHost.class, host); - registerService(Application.class, host.createApplication()); - registerService(HardwareSurface.class, host.createHardwareSurface()); - registerService(Layers.class, new Layers(extension)); - registerService(Transport.class, host.createTransport()); - registerService(Project.class, host.getProject()); - } - - public ViewTracker getViewTracker() { - if (viewTrackerGlobal == null) { - viewTrackerGlobal = new ViewTrackerImpl(); - } - return viewTrackerGlobal; - } - - /** - * Retrieves an existing service/component. If the component isn't registered null is returned - * - * @param type the type the component/service is registered under. - * @param the overall type of the service - * @return the component/service. - */ - @SuppressWarnings("unchecked") - public T getService(final Class type) { - return (T) services.get(type); - } - - /** - * Simply registers an object under a given service type. - * - * @param type the service type (needs to be an interface) - * @param object the object to be registered - * @param the type - */ - public void registerService(final Class type, final T object) { - services.put(type, object); - tryToCompleteIncompleteClosures(type); - tryToCompleteIncompleteSetter(type); - } - - /** - * Creates a standard Layer given context - * - * @param name name of the layer. - * @return the newly created layer. - */ - public Layer createLayer(final String name) { - return new Layer(getService(Layers.class), name); - } - - /** - * Creates a component and registers it under its class in the context. - * - * @param clazzToCreate the component to create. - * @param the overall service type - * @return the created component. - */ - public T create(final Class clazzToCreate) { - final ComponentClosure closure = new ComponentClosure<>(clazzToCreate); - create(closure); - if (closure.instance != null) { - registerService(closure.instance); - tryToCompleteIncompleteClosures(clazzToCreate); - tryToCompleteIncompleteSetter(clazzToCreate); - } - return closure.instance; - } - - @SuppressWarnings("unchecked") - private void create(final ComponentClosure closure) { - final Class clazzToCreate = closure.clazz; - final Component componentAnnotation = clazzToCreate.getAnnotation(Component.class); - try { - final Constructor[] constructors = clazzToCreate.getConstructors(); - T object = null; - - Constructor lastConstructor = null; - for (final Constructor constructor : constructors) { - lastConstructor = constructor; - final Object[] args = getConstructableArguments(constructor, componentAnnotation); - if (args != null) { - object = (T) constructor.newInstance(args); - break; + } + } + + private boolean invokeActivation(final Object comp, final Class annotation) { + final Class clazz = comp.getClass(); + final Method[] methods = clazz.getMethods(); + for (final Method method : methods) { + final T activateAnnotation = method.getAnnotation(annotation); + if (activateAnnotation != null && method.getParameterCount() == 0) { + try { + method.invoke(comp); + return true; + } + catch (final IllegalAccessException | InvocationTargetException exception) { + throw new DiException("failed to activate component " + clazz.getName(), exception); + } } - } - if (lastConstructor == null) { - throw new DiException( - String.format("Class %s has not available constructor", closure.clazz.getSimpleName())); - } - closure.missingComponents.addAll(findMissingFieldInjection(closure)); - - if (object == null) { - closure.missingComponents.addAll(getMissingConstructorTypes(lastConstructor)); - return; - } - - if (!closure.missingComponents.isEmpty()) { - return; - } - - final Field[] fields = clazzToCreate.getDeclaredFields(); - for (final Field field : fields) { - final Inject injectAnnotation = field.getAnnotation(Inject.class); - if (injectAnnotation != null) { - final boolean success = inject(object, field); - if (!success) { - closure.missingComponents.add(field.getType()); - } + } + return false; + } + + private record ComponentClassBind(Class clazz, Component component) implements Comparable { + // + static Optional create(final Class clazz) { + final Component serviceAnnotation = clazz.getAnnotation(Component.class); + if (serviceAnnotation != null && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) { + return Optional.of(new ComponentClassBind(clazz, serviceAnnotation)); } - } - - injectBySetter(object, closure); - - final Method[] methods = clazzToCreate.getDeclaredMethods(); - for (final Method method : methods) { - final PostConstruct postConstruct = method.getAnnotation(PostConstruct.class); - if (postConstruct != null && !Modifier.isPrivate(method.getModifiers())) { - invokePostConstruct(object, method); + return Optional.empty(); + } + + @Override + public int compareTo(final ComponentClassBind o) { + return o.component.priority() - component.priority(); + } + } + + private void scanPackage(final Class baseClass, final Package... packages) { + try { + final List> classes = PackageHelper.getClasses(baseClass, packages); + final List components = classes.stream()// + .map(ComponentClassBind::create) // + .flatMap(o -> o.stream()) // + .filter(this::matchesTags) // + .sorted() // + .toList(); + for (final ComponentClassBind bind : components) { + createAnnotatedComponent(bind.clazz()); + registerInterfacesToService(bind.clazz()); + } + } + catch (final IOException | ClassNotFoundException exception) { + throw new DiException("Failed to create annotated components", exception); + } + } + + private boolean matchesTags(final ComponentClassBind component) { + if (component.component().tag().isBlank()) { + return true; + } + return tags.contains(component.component.tag()); + } + + private void registerInterfacesToService(final Class clazz) { + final List> classInterfaces = getInterfaces(clazz); + for (final Class interfaceClass : classInterfaces) { + serviceTypes.put(interfaceClass, clazz); + } + } + + private void initializeExtension(final ControllerExtension extension) { + final ControllerHost host = extension.getHost(); + registerService(ControllerHost.class, host); + registerService(Application.class, host.createApplication()); + registerService(HardwareSurface.class, host.createHardwareSurface()); + registerService(Layers.class, new Layers(extension)); + registerService(Transport.class, host.createTransport()); + registerService(Project.class, host.getProject()); + } + + public ViewTracker getViewTracker() { + if (viewTrackerGlobal == null) { + viewTrackerGlobal = new ViewTrackerImpl(); + } + return viewTrackerGlobal; + } + + /** + * Retrieves an existing service/component. If the component isn't registered null is returned + * + * @param type the type the component/service is registered under. + * @param the overall type of the service + * @return the component/service. + */ + @SuppressWarnings("unchecked") + public T getService(final Class type) { + return (T) services.get(type); + } + + /** + * Simply registers an object under a given service type. + * + * @param type the service type (needs to be an interface) + * @param object the object to be registered + * @param the type + */ + public void registerService(final Class type, final T object) { + services.put(type, object); + tryToCompleteIncompleteClosures(type); + tryToCompleteIncompleteSetter(type); + } + + /** + * Creates a standard Layer given context + * + * @param name name of the layer. + * @return the newly created layer. + */ + public Layer createLayer(final String name) { + return new Layer(getService(Layers.class), name); + } + + /** + * Creates a component and registers it under its class in the context. + * + * @param clazzToCreate the component to create. + * @param the overall service type + * @return the created component. + */ + public T create(final Class clazzToCreate) { + final ComponentClosure closure = new ComponentClosure<>(clazzToCreate); + create(closure); + if (closure.instance != null) { + registerService(closure.instance); + tryToCompleteIncompleteClosures(clazzToCreate); + tryToCompleteIncompleteSetter(clazzToCreate); + } + return closure.instance; + } + + @SuppressWarnings("unchecked") + private void create(final ComponentClosure closure) { + final Class clazzToCreate = closure.clazz; + final Component componentAnnotation = clazzToCreate.getAnnotation(Component.class); + try { + final Constructor[] constructors = clazzToCreate.getConstructors(); + T object = null; + + Constructor lastConstructor = null; + for (final Constructor constructor : constructors) { + lastConstructor = constructor; + final Object[] args = getConstructableArguments(constructor, componentAnnotation); + if (args != null) { + object = (T) constructor.newInstance(args); + break; + } + } + if (lastConstructor == null) { + throw new DiException( + String.format("Class %s has not available constructor", closure.clazz.getSimpleName())); + } + closure.missingComponents.addAll(findMissingFieldInjection(closure)); + + if (object == null) { + closure.missingComponents.addAll(getMissingConstructorTypes(lastConstructor)); + return; + } + + if (!closure.missingComponents.isEmpty()) { + return; + } + + final Field[] fields = clazzToCreate.getDeclaredFields(); + for (final Field field : fields) { + final Inject injectAnnotation = field.getAnnotation(Inject.class); + if (injectAnnotation != null) { + final boolean success = inject(object, field); + if (!success) { + closure.missingComponents.add(field.getType()); + } + } } - } - closure.instance = object; - } catch (final SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | + + injectBySetter(object, closure); + + final Method[] methods = clazzToCreate.getDeclaredMethods(); + for (final Method method : methods) { + final PostConstruct postConstruct = method.getAnnotation(PostConstruct.class); + if (postConstruct != null && !Modifier.isPrivate(method.getModifiers())) { + invokePostConstruct(object, method); + } + } + closure.instance = object; + } + catch (final SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new DiException("failed to create component object", e); - } - } - - private void registerService(final T object) { - final Class mainType = object.getClass(); - final List> interfaces = getInterfaces(mainType); - final Component annotation = mainType.getAnnotation(Component.class); - services.put(mainType, object); - if (annotation != null) { - for (final Class interfaceType : interfaces) { - services.put(interfaceType, object); - } - } - } - - private void createAnnotatedComponent(final Class clazzToCreate) { - final ComponentClosure closure = new ComponentClosure<>(clazzToCreate); - create(closure); - verifyIncompleteSetters(closure); - if (closure.instance != null) { - registerService(closure.instance); - tryToCompleteIncompleteClosures(clazzToCreate); - tryToCompleteIncompleteSetter(clazzToCreate); - } else { - incompleteClosures.add(closure); - } - } - - private void verifyIncompleteSetters(final ComponentClosure closure) { - if (!closure.missingSetters.isEmpty() && !incompleteSetterClosures.contains(closure)) { - incompleteSetterClosures.add(closure); - } - } - - private List> findMissingFieldInjection(final ComponentClosure closure) { - final List> missingComponents = new ArrayList<>(); - final Field[] fields = closure.clazz.getDeclaredFields(); - for (final Field field : fields) { - final Inject injectAnnotation = field.getAnnotation(Inject.class); - if (injectAnnotation != null) { - final Object serviceObject = getServiceImpl(field.getType()); - if (serviceObject == null) { - missingComponents.add(field.getType()); + throw new DiException("failed to create component object", e); + } + } + + private void registerService(final T object) { + final Class mainType = object.getClass(); + final List> interfaces = getInterfaces(mainType); + final Component annotation = mainType.getAnnotation(Component.class); + services.put(mainType, object); + if (annotation != null) { + for (final Class interfaceType : interfaces) { + services.put(interfaceType, object); } - } - } - return missingComponents; - } - - private void tryToCompleteIncompleteClosures(final Class newType) { - if (incompleteClosures.isEmpty()) { - return; - } - final Iterator> it = incompleteClosures.iterator(); - final List> completedClosures = new ArrayList<>(); - while (it.hasNext()) { - final ComponentClosure closure = it.next(); - if (closure.missingComponents.remove(newType) && closure.missingComponents.isEmpty()) { - create(closure); + } + } + + private void createAnnotatedComponent(final Class clazzToCreate) { + final ComponentClosure closure = new ComponentClosure<>(clazzToCreate); + create(closure); + verifyIncompleteSetters(closure); + if (closure.instance != null) { registerService(closure.instance); - completedClosures.add(closure); - verifyIncompleteSetters(closure); - it.remove(); - } - } - for (final ComponentClosure completed : completedClosures) { - tryToCompleteIncompleteClosures(completed.clazz); - tryToCompleteIncompleteSetter(completed.clazz); - } - } - - private void tryToCompleteIncompleteSetter(final Class newType) { - if (incompleteSetterClosures.isEmpty()) { - return; - } - final Iterator> it = incompleteSetterClosures.iterator(); - while (it.hasNext()) { - final ComponentClosure closure = it.next(); - if (closure.instance != null && closure.missingSetters.remove(newType) - && closure.missingSetters.isEmpty()) { - try { - injectBySetter(closure.instance, closure); - it.remove(); - } catch (final IllegalAccessException | InvocationTargetException exception) { - throw new DiException("Failed setter injection ", exception); + tryToCompleteIncompleteClosures(clazzToCreate); + tryToCompleteIncompleteSetter(clazzToCreate); + } else { + incompleteClosures.add(closure); + } + } + + private void verifyIncompleteSetters(final ComponentClosure closure) { + if (!closure.missingSetters.isEmpty() && !incompleteSetterClosures.contains(closure)) { + incompleteSetterClosures.add(closure); + } + } + + private List> findMissingFieldInjection(final ComponentClosure closure) { + final List> missingComponents = new ArrayList<>(); + final Field[] fields = closure.clazz.getDeclaredFields(); + for (final Field field : fields) { + final Inject injectAnnotation = field.getAnnotation(Inject.class); + if (injectAnnotation != null) { + final Object serviceObject = getServiceImpl(field.getType()); + if (serviceObject == null) { + missingComponents.add(field.getType()); + } } - } - } - } - - private void injectBySetter(final Object object, final ComponentClosure closure) throws - IllegalAccessException, - InvocationTargetException { - final Method[] methods = closure.clazz.getMethods(); - - for (final Method method : methods) { - final Inject injectNotation = method.getAnnotation(Inject.class); - if (injectNotation != null && !Modifier.isPrivate(method.getModifiers()) - && method.getParameterCount() == 1) { + } + return missingComponents; + } + + private void tryToCompleteIncompleteClosures(final Class newType) { + if (incompleteClosures.isEmpty()) { + return; + } + final Iterator> it = incompleteClosures.iterator(); + final List> completedClosures = new ArrayList<>(); + while (it.hasNext()) { + final ComponentClosure closure = it.next(); + if (closure.missingComponents.remove(newType) && closure.missingComponents.isEmpty()) { + create(closure); + registerService(closure.instance); + completedClosures.add(closure); + verifyIncompleteSetters(closure); + it.remove(); + } + } + for (final ComponentClosure completed : completedClosures) { + tryToCompleteIncompleteClosures(completed.clazz); + tryToCompleteIncompleteSetter(completed.clazz); + } + } + + private void tryToCompleteIncompleteSetter(final Class newType) { + if (incompleteSetterClosures.isEmpty()) { + return; + } + final Iterator> it = incompleteSetterClosures.iterator(); + while (it.hasNext()) { + final ComponentClosure closure = it.next(); + if (closure.instance != null && closure.missingSetters.remove(newType) + && closure.missingSetters.isEmpty()) { + try { + injectBySetter(closure.instance, closure); + it.remove(); + } + catch (final IllegalAccessException | InvocationTargetException exception) { + throw new DiException("Failed setter injection ", exception); + } + } + } + } + + private void injectBySetter(final Object object, final ComponentClosure closure) throws + IllegalAccessException, + InvocationTargetException { + final Method[] methods = closure.clazz.getMethods(); + + for (final Method method : methods) { + final Inject injectNotation = method.getAnnotation(Inject.class); + if (injectNotation != null && !Modifier.isPrivate(method.getModifiers()) + && method.getParameterCount() == 1) { + final Object[] args = getMethodServiceArguments(method); + if (args != null) { + method.invoke(object, args); + } else { + closure.missingSetters.addAll(getMissingTypes(method)); + } + } + } + } + + private void invokePostConstruct(final T object, final Method method) throws + IllegalAccessException, + InvocationTargetException { + if (method.getParameterCount() == 0) { + method.setAccessible(true); + method.invoke(object); + method.setAccessible(false); + } else { + // TODO PostConstruct is not covered when incomplete, maybe then it will not be called final Object[] args = getMethodServiceArguments(method); if (args != null) { - method.invoke(object, args); + method.setAccessible(true); + method.invoke(object, args); + method.setAccessible(false); + } + } + } + + private List> getMissingConstructorTypes(final Constructor constructor) { + final List> missingTypes = new ArrayList<>(); + final Parameter[] parameterTypes = constructor.getParameters(); + for (final Parameter parameterType : parameterTypes) { + final Class paramType = parameterType.getType(); + final Object constructorObject = getServiceImpl(paramType); + if (constructorObject == null && paramType != String.class) { + missingTypes.add(paramType); + } + } + + return missingTypes; + } + + private List> getInterfaces(final Class clazz) { + final List> interfaces = new ArrayList<>(); + final Type[] classInterfaces = clazz.getGenericInterfaces(); + for (final Type interfaceClass : classInterfaces) { + if (interfaceClass instanceof Class) { + interfaces.add((Class) interfaceClass); + } + } + if (clazz.getSuperclass() != Object.class) { + interfaces.addAll(getInterfaces(clazz.getSuperclass())); + } + return interfaces; + } + + private Object[] getConstructableArguments(final Constructor constructor, final Component compAnnotation) { + final Parameter[] parameterTypes = constructor.getParameters(); + final Object[] args = new Object[constructor.getParameterCount()]; + boolean foundName = false; + for (int i = 0; i < parameterTypes.length; i++) { + final Class paramType = parameterTypes[i].getType(); + Object constructorObject = getServiceImpl(paramType); + if (constructorObject == null && paramType == String.class && !foundName && compAnnotation != null + && !compAnnotation.name().isBlank()) { + constructorObject = compAnnotation.name(); + foundName = true; + } else if (constructorObject == null && paramType != String.class) { + return null; + } + args[i] = constructorObject; + } + + return args; + } + + private List> getMissingTypes(final Method method) { + final List> missingComponents = new ArrayList<>(); + for (final Parameter parameterType : method.getParameters()) { + final Class paramType = parameterType.getType(); + final Object toSetService = getServiceImpl(paramType); + if (toSetService == null) { + missingComponents.add(paramType); + } + } + + return missingComponents; + } + + private Object[] getMethodServiceArguments(final Method method) { + final Parameter[] parameterTypes = method.getParameters(); + final Object[] args = new Object[method.getParameterCount()]; + for (int i = 0; i < parameterTypes.length; i++) { + final Class paramType = parameterTypes[i].getType(); + final Object toSetService = getServiceImpl(paramType); + if (toSetService == null && paramType != String.class) { + return null; + } + args[i] = toSetService; + } + + return args; + } + + private boolean inject(final T object, final Field field) throws + IllegalArgumentException, + IllegalAccessException { + @SuppressWarnings("unchecked") final T serviceObject = (T) getServiceImpl(field.getType()); + //TODO make injection possibly Optional + if (serviceObject != null) { + field.setAccessible(true); + field.set(object, serviceObject); + field.setAccessible(false); + return true; + } + return false; + } + + @SuppressWarnings("unchecked") + private T getServiceImpl(final Class serviceType) { + final T serviceObject = (T) services.get(serviceType); + if (serviceObject != null) { + return serviceObject; + } + final Class serviceClass = serviceTypes.get(serviceType); + if (serviceClass != null) { + final ComponentClosure closure = new ComponentClosure<>(serviceClass); + create(closure); + if (closure.instance != null) { + services.put(serviceType, closure.instance); + verifyIncompleteSetters(closure); + return closure.instance; } else { - closure.missingSetters.addAll(getMissingTypes(method)); + incompleteClosures.add(closure); } - } - } - } - - private void invokePostConstruct(final T object, final Method method) throws - IllegalAccessException, - InvocationTargetException { - if (method.getParameterCount() == 0) { - method.setAccessible(true); - method.invoke(object); - method.setAccessible(false); - } else { - // TODO PostConstruct is not covered when incomplete, maybe then it will not be called - final Object[] args = getMethodServiceArguments(method); - if (args != null) { - method.setAccessible(true); - method.invoke(object, args); - method.setAccessible(false); - } - } - } - - private List> getMissingConstructorTypes(final Constructor constructor) { - final List> missingTypes = new ArrayList<>(); - final Parameter[] parameterTypes = constructor.getParameters(); - for (final Parameter parameterType : parameterTypes) { - final Class paramType = parameterType.getType(); - final Object constructorObject = getServiceImpl(paramType); - if (constructorObject == null && paramType != String.class) { - missingTypes.add(paramType); - } - } - - return missingTypes; - } - - private List> getInterfaces(final Class clazz) { - final List> interfaces = new ArrayList<>(); - final Type[] classInterfaces = clazz.getGenericInterfaces(); - for (final Type interfaceClass : classInterfaces) { - if (interfaceClass instanceof Class) { - interfaces.add((Class) interfaceClass); - } - } - if (clazz.getSuperclass() != Object.class) { - interfaces.addAll(getInterfaces(clazz.getSuperclass())); - } - return interfaces; - } - - private Object[] getConstructableArguments(final Constructor constructor, final Component compAnnotation) { - final Parameter[] parameterTypes = constructor.getParameters(); - final Object[] args = new Object[constructor.getParameterCount()]; - boolean foundName = false; - for (int i = 0; i < parameterTypes.length; i++) { - final Class paramType = parameterTypes[i].getType(); - Object constructorObject = getServiceImpl(paramType); - if (constructorObject == null && paramType == String.class && !foundName && compAnnotation != null - && !compAnnotation.name().isBlank()) { - constructorObject = compAnnotation.name(); - foundName = true; - } else if (constructorObject == null && paramType != String.class) { - return null; - } - args[i] = constructorObject; - } - - return args; - } - - private List> getMissingTypes(final Method method) { - final List> missingComponents = new ArrayList<>(); - for (final Parameter parameterType : method.getParameters()) { - final Class paramType = parameterType.getType(); - final Object toSetService = getServiceImpl(paramType); - if (toSetService == null) { - missingComponents.add(paramType); - } - } - - return missingComponents; - } - - private Object[] getMethodServiceArguments(final Method method) { - final Parameter[] parameterTypes = method.getParameters(); - final Object[] args = new Object[method.getParameterCount()]; - for (int i = 0; i < parameterTypes.length; i++) { - final Class paramType = parameterTypes[i].getType(); - final Object toSetService = getServiceImpl(paramType); - if (toSetService == null && paramType != String.class) { - return null; - } - args[i] = toSetService; - } - - return args; - } - - private boolean inject(final T object, final Field field) throws - IllegalArgumentException, - IllegalAccessException { - @SuppressWarnings("unchecked") final T serviceObject = (T) getServiceImpl(field.getType()); - //TODO make injection possibly Optional - if (serviceObject != null) { - field.setAccessible(true); - field.set(object, serviceObject); - field.setAccessible(false); - return true; - } - return false; - } - - @SuppressWarnings("unchecked") - private T getServiceImpl(final Class serviceType) { - final T serviceObject = (T) services.get(serviceType); - if (serviceObject != null) { - return serviceObject; - } - final Class serviceClass = serviceTypes.get(serviceType); - if (serviceClass != null) { - final ComponentClosure closure = new ComponentClosure<>(serviceClass); - create(closure); - if (closure.instance != null) { - services.put(serviceType, closure.instance); - verifyIncompleteSetters(closure); - return closure.instance; - } else { - incompleteClosures.add(closure); - } - } - return null; - } - - + } + return null; + } + + } diff --git a/src/main/java/com/bitwig/extensions/framework/time/AbstractTimedEvent.java b/src/main/java/com/bitwig/extensions/framework/time/AbstractTimedEvent.java index d7a2bc1f..34039604 100644 --- a/src/main/java/com/bitwig/extensions/framework/time/AbstractTimedEvent.java +++ b/src/main/java/com/bitwig/extensions/framework/time/AbstractTimedEvent.java @@ -1,22 +1,26 @@ package com.bitwig.extensions.framework.time; public abstract class AbstractTimedEvent implements TimedEvent { - protected final long startTime; - protected boolean completed; - protected final long delayTime; - - public AbstractTimedEvent(final long delayTime) { - startTime = System.currentTimeMillis(); - completed = false; - this.delayTime = delayTime; - } - - public void cancel() { - completed = true; - } - - public boolean isCompleted() { - return completed; - } - + protected long startTime; + protected boolean completed; + protected final long delayTime; + + public AbstractTimedEvent(final long delayTime) { + startTime = System.currentTimeMillis(); + completed = false; + this.delayTime = delayTime; + } + + public void resetTime() { + startTime = System.currentTimeMillis(); + } + + public void cancel() { + completed = true; + } + + public boolean isCompleted() { + return completed; + } + } diff --git a/src/main/java/com/bitwig/extensions/framework/values/EnumeratorValue.java b/src/main/java/com/bitwig/extensions/framework/values/EnumeratorValue.java new file mode 100644 index 00000000..1a852a7d --- /dev/null +++ b/src/main/java/com/bitwig/extensions/framework/values/EnumeratorValue.java @@ -0,0 +1,61 @@ +package com.bitwig.extensions.framework.values; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class EnumeratorValue { + + private List elements = new ArrayList<>(); + private final List> callbacks = new ArrayList<>(); + private int index; + + public EnumeratorValue(final T[] values) { + this.elements = Arrays.stream(values).toList(); + } + + public EnumeratorValue(final List values) { + this.elements = values.stream().toList(); + } + + public void addValueObserver(final Consumer callback) { + if (!callbacks.contains(callback)) { + callbacks.add(callback); + } + } + + public void set(final T value) { + final int setIndex = elements.indexOf(value); + if (setIndex == -1 || setIndex == index) { + return; + } + this.index = setIndex; + for (final Consumer listener : callbacks) { + listener.accept(value); + } + } + + public T get() { + return elements.get(index); + } + + public void increment(final int inc, final boolean roundRobin) { + int next = index + inc; + if (roundRobin) { + if (next < 0) { + next = elements.size() - 1; + } else if (next >= elements.size()) { + next = 0; + } + } else { + if (next < 0 || next >= elements.size()) { + return; + } + } + this.index = next; + for (final Consumer listener : callbacks) { + listener.accept(elements.get(index)); + } + } +} diff --git a/src/main/java/com/bitwig/extensions/framework/values/IntValueObject.java b/src/main/java/com/bitwig/extensions/framework/values/IntValueObject.java index b399e528..0805585b 100644 --- a/src/main/java/com/bitwig/extensions/framework/values/IntValueObject.java +++ b/src/main/java/com/bitwig/extensions/framework/values/IntValueObject.java @@ -2,75 +2,72 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.IntConsumer; public class IntValueObject implements IncrementalValue { - public interface IntChangeCallback { - void valueChanged(int oldValue, int newValue); - } - - @FunctionalInterface - public interface Converter { - String convert(int value); - } - - private final List callbacks = new ArrayList<>(); - private int value; - private final int min; - private final int max; - private final Converter converter; - - public IntValueObject(final int initValue, final int min, final int max) { - this.value = initValue; - this.min = min; - this.max = max; - this.converter = null; - } - - public IntValueObject(final int initValue, final int min, final int max, final Converter converter) { - this.value = initValue; - this.min = min; - this.max = max; - this.converter = converter; - } - - public int getMax() { - return max; - } - - public void addValueObserver(final IntChangeCallback callback) { - if (!callbacks.contains(callback)) { - callbacks.add(callback); - } - } - - public void set(final int value) { - final int newValue = Math.max(min, Math.min(max, value)); - if (this.value == newValue) { - return; - } - final int oldValue = this.value; - this.value = newValue; - for (final IntChangeCallback listener : callbacks) { - listener.valueChanged(oldValue, value); - } - } - - @Override - public void increment(final int amount) { - final int newValue = Math.max(min, Math.min(max, value + amount)); - this.set(newValue); - } - - public int get() { - return value; - } - - @Override - public String displayedValue() { - if (converter != null) { - return converter.convert(value); - } - return Integer.toString(value); - } - + + @FunctionalInterface + public interface Converter { + String convert(int value); + } + + private final List callbacks = new ArrayList<>(); + private int value; + private final int min; + private final int max; + private final Converter converter; + + public IntValueObject(final int initValue, final int min, final int max) { + this.value = initValue; + this.min = min; + this.max = max; + this.converter = null; + } + + public IntValueObject(final int initValue, final int min, final int max, final Converter converter) { + this.value = initValue; + this.min = min; + this.max = max; + this.converter = converter; + } + + public int getMax() { + return max; + } + + public void addValueObserver(final IntConsumer callback) { + if (!callbacks.contains(callback)) { + callbacks.add(callback); + } + } + + public void set(final int value) { + final int newValue = Math.max(min, Math.min(max, value)); + if (this.value == newValue) { + return; + } + this.value = newValue; + for (final IntConsumer listener : callbacks) { + listener.accept(value); + } + } + + @Override + public void increment(final int amount) { + final int newValue = Math.max(min, Math.min(max, value + amount)); + this.set(newValue); + } + + public int get() { + return value; + } + + @Override + public String displayedValue() { + if (converter != null) { + return converter.convert(value); + } + return Integer.toString(value); + } + } diff --git a/src/main/java/com/bitwig/extensions/framework/values/Scale.java b/src/main/java/com/bitwig/extensions/framework/values/Scale.java index f5547419..8da7f4e9 100644 --- a/src/main/java/com/bitwig/extensions/framework/values/Scale.java +++ b/src/main/java/com/bitwig/extensions/framework/values/Scale.java @@ -3,14 +3,14 @@ public enum Scale implements IScale { CHROMATIC("Chromatic", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), // - MAJOR("Ionian/Major", 0, 2, 4, 5, 7, 9, 11), // - MINOR("Aeolian/Minor", 0, 2, 3, 5, 7, 8, 10), // + MAJOR("Ionian/Major", "Major", 0, 2, 4, 5, 7, 9, 11), // + MINOR("Aeolian/Minor", "Minor", 0, 2, 3, 5, 7, 8, 10), // PENTATONIC("Pentatonic", 0, 2, 4, 7, 9), // - PENTATONIC_MINOR("Pentatonic Minor", 0, 3, 5, 7, 10), // - DORIAN("Dorian (B/g)", 0, 2, 3, 5, 7, 9, 10), // - PHRYGIAN("Phrygian (A-flat/f)", 0, 1, 3, 5, 7, 8, 10), // - LYDIAN("Lydian (D/e)", 0, 2, 4, 6, 7, 9, 11), // - MIXOLYDIAN("Mixolydian (F/d)", 0, 2, 4, 5, 7, 9, 10), // + PENTATONIC_MINOR("Pentatonic Minor", "Pent.Min", 0, 3, 5, 7, 10), // + DORIAN("Dorian (B/g)", "Dorian", 0, 2, 3, 5, 7, 9, 10), // + PHRYGIAN("Phrygian (A-flat/f)", "Phrygian", 0, 1, 3, 5, 7, 8, 10), // + LYDIAN("Lydian (D/e)", "Lydian", 0, 2, 4, 6, 7, 9, 11), // + MIXOLYDIAN("Mixolydian (F/d)", "Mixolydian", 0, 2, 4, 5, 7, 9, 10), // LOCRIAN("Locrian", 0, 1, 3, 5, 6, 8, 10), // DIMINISHED("Diminished", 0, 2, 3, 5, 6, 8, 9, 10), // MAJOR_BLUES("Major Blues", 0, 3, 4, 7, 9, 10), // @@ -23,11 +23,17 @@ public enum Scale implements IScale { private final String name; + private final String shortName; private final int[] intervals; private final boolean[] inscaleMatch = new boolean[12]; Scale(final String name, final int... notes) { + this(name, name, notes); + } + + Scale(final String name, final String shortName, final int... notes) { this.name = name; + this.shortName = shortName; this.intervals = notes; for (int i = 0; i < this.intervals.length; i++) { inscaleMatch[this.intervals[i] % 12] = true; @@ -44,6 +50,10 @@ public String getName() { return name; } + public String getShortName() { + return shortName; + } + @Override public int[] getIntervals() { return intervals; diff --git a/src/main/resources/Documentation/Controllers/Akai/AD41 Ableton Live Developers Reference 1.07.pdf b/src/main/resources/Documentation/Controllers/Akai/AD41 Ableton Live Developers Reference 1.07.pdf new file mode 100644 index 00000000..ad22d138 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/Akai/AD41 Ableton Live Developers Reference 1.07.pdf differ diff --git a/src/main/resources/Documentation/Controllers/Akai/MPK Mini Mk4.pdf b/src/main/resources/Documentation/Controllers/Akai/MPK Mini Mk4.pdf new file mode 100644 index 00000000..d9c09e73 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/Akai/MPK Mini Mk4.pdf differ diff --git a/src/main/resources/Documentation/Controllers/AllenHeath/Allen & Heath Xone K3.pdf b/src/main/resources/Documentation/Controllers/AllenHeath/Allen & Heath Xone K3.pdf new file mode 100644 index 00000000..9956db50 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/AllenHeath/Allen & Heath Xone K3.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 7f88096e..24de40af 100644 --- a/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition +++ b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition @@ -1,3 +1,6 @@ +com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3ExtensionDefinition +com.bitwig.extensions.controllers.allenheath.xonek3.XoneK3ExtensionX2Definition + com.bitwig.extensions.controllers.akai.advance.AdvanceControllerExtensionDefinition com.bitwig.extensions.controllers.akai.apc40_mkii.APC40MKIIControllerExtensionDefinition com.bitwig.extensions.controllers.akai.mpk_mini_mk3.MpkMiniMk3ControllerExtensionDefinition @@ -5,6 +8,7 @@ com.bitwig.extensions.controllers.akai.mpkminiplus.MpkMiniPlusControllerExtensio com.bitwig.extensions.controllers.akai.apcmk2.AkaiApcKeys25Definition com.bitwig.extensions.controllers.akai.apcmk2.AkaiApcMiniDefinition com.bitwig.extensions.controllers.akai.apc64.Apc64ExtensionDefinition +com.bitwig.extensions.controllers.akai.mpkmk4.MpkMiniMk4ControllerExtensionDefinition com.bitwig.extensions.controllers.arturia.keylab.mk1.ArturiaKeylab25ControllerExtensionDefinition com.bitwig.extensions.controllers.arturia.keylab.mk2.ArturiaKeylab49MkIIControllerExtensionDefinition @@ -128,7 +132,8 @@ com.bitwig.extensions.controllers.novation.launchpadmini3.LaunchPadMiniMk3Extens com.bitwig.extensions.controllers.novation.launchpadmini3.LaunchPadXExtensionDefinition com.bitwig.extensions.controllers.novation.slmk3.SlMk3ExtensionDefinition -com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.LaunchControlXlMk3ExtensionDefinition +com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.LaunchControlXlExtensionDefinition +#com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.LaunchControlExtensionDefinition com.bitwig.extensions.controllers.presonus.atom.PresonusAtomDefinition com.bitwig.extensions.controllers.presonus.faderport.PresonusFaderPort8Definition